Skip to main content

magellan/graph/
algorithms.rs

1//! Graph algorithms for code analysis
2//!
3//! This module provides wrapper functions for sqlitegraph's algorithm library,
4//! exposing reachability analysis, dead code detection, cycle detection, call
5//! graph condensation, and impact analysis through Magellan's CodeGraph API.
6//!
7//! # Graph Views
8//!
9//! Magellan stores multiple edge types in the same graph:
10//! - **DEFINES**: File → Symbol (defines/contains relationship)
11//! - **REFERENCES**: Reference → Symbol (what symbol is referenced)
12//! - **CALLS**: Call node → Symbol (call graph edges)
13//! - **CALLER**: Symbol → Call node (reverse call graph edges)
14//!
15//! For call graph algorithms (reachability, dead code detection, SCC), we filter
16//! to **CALLS** edges only to traverse the call graph structure.
17//!
18//! # Clustered Adjacency
19//!
20//! Magellan uses sqlitegraph's clustered adjacency storage for ~10x graph
21//! traversal performance improvement when available.
22//!
23//! # Entity IDs vs Symbol IDs
24//!
25//! sqlitegraph algorithms work with **entity IDs** (i64 database row IDs),
26//! while Magellan's public API uses **stable symbol IDs** (32-char BLAKE3 hashes).
27//!
28//! This module provides translation functions:
29//! - `resolve_symbol_entity()`: Symbol ID → entity ID
30//! - `symbol_by_entity_id()`: entity ID → SymbolInfo
31//!
32//! # Algorithm Functions
33//!
34//! - [`CodeGraph::reachable_symbols()`]: Forward reachability from a symbol
35//! - [`CodeGraph::reverse_reachable_symbols()`]: Reverse reachability (callers)
36//! - [`CodeGraph::dead_symbols()`]: Dead code detection from entry point
37//! - [`CodeGraph::detect_cycles()`]: Find cycles using SCC decomposition
38//! - [`CodeGraph::find_cycles_containing()`]: Find cycles containing a specific symbol
39//! - [`CodeGraph::condense_call_graph()`]: Collapse SCCs to create condensation DAG
40//! - [`CodeGraph::enumerate_paths()`]: Path enumeration between symbols
41//! - [`CodeGraph::backward_slice()`]: Backward program slice (what affects this symbol)
42//! - [`CodeGraph::forward_slice()`]: Forward program slice (what this symbol affects)
43//!
44//! # Example
45//!
46//! \`\`\`no_run
47//! use magellan::CodeGraph;
48//!
49//! let mut graph = CodeGraph::open("codegraph.db")?;
50//!
51//! // Find all functions reachable from main
52//! let reachable = graph.reachable_symbols("main_symbol_id", None)?;
53//!
54//! // Find dead code unreachable from main
55//! let dead = graph.dead_symbols("main_symbol_id")?;
56//!
57//! // Find cycles (mutual recursion)
58//! let cycles = graph.detect_cycles()?;
59//!
60//! // Condense call graph for DAG analysis
61//! let condensed = graph.condense_call_graph()?;
62//! \`\`\`
63
64use anyhow::Result;
65use ahash::AHashSet;
66use rusqlite::params;
67use sqlitegraph::{algo, GraphBackend, SnapshotId, SqliteGraphBackend};
68use std::collections::{HashMap, HashSet};
69use std::sync::Arc;
70
71use crate::graph::schema::SymbolNode;
72
73use super::CodeGraph;
74
75/// Helper to get the underlying SqliteGraph from a trait object
76///
77/// This is a temporary workaround until algorithm methods are added to GraphBackend trait.
78/// For SQLite backend, this extracts the concrete SqliteGraphBackend and calls .graph().
79///
80/// # Safety
81/// 
82/// This function uses unsafe pointer casting because the GraphBackend trait doesn't
83/// provide downcasting methods. It's safe as long as:
84/// - The backend is actually a SqliteGraphBackend (not V3 or other backend)
85/// - The Arc hasn't been cloned/reinterpreted
86///
87/// TODO: Remove this when sqlitegraph adds `as_any()` to GraphBackend trait
88fn get_sqlite_graph(backend: &Arc<dyn GraphBackend>) -> Result<&sqlitegraph::graph::SqliteGraph, anyhow::Error> {
89    // SAFETY: This is a temporary workaround. We use pointer casting to downcast
90    // from Arc<dyn GraphBackend> to Arc<SqliteGraphBackend>. This is safe because:
91    // 1. We only call this when we know the backend is SqliteGraphBackend
92    // 2. The Arc layout is stable for trait objects
93    // 3. We only read, don't modify
94    unsafe {
95        let ptr = Arc::as_ptr(backend);
96        // Double-check pointer is non-null before dereferencing
97        if ptr.is_null() {
98            return Err(anyhow::anyhow!("Null backend pointer"));
99        }
100        // Reinterpret as SqliteGraphBackend pointer
101        let concrete = &*(ptr as *const SqliteGraphBackend);
102        Ok(concrete.graph())
103    }
104}
105
106/// Symbol information for algorithm results
107///
108/// Contains the key metadata needed to identify and locate a symbol.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct SymbolInfo {
111    /// Stable symbol ID (32-char BLAKE3 hash)
112    pub symbol_id: Option<String>,
113    /// Fully-qualified name
114    pub fqn: Option<String>,
115    /// File path containing the symbol
116    pub file_path: String,
117    /// Symbol kind (Function, Method, Class, etc.)
118    pub kind: String,
119}
120
121/// Dead symbol information
122///
123/// Extends [`SymbolInfo`] with a reason why the symbol is considered dead.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct DeadSymbol {
126    /// Base symbol information
127    pub symbol: SymbolInfo,
128    /// Reason why this symbol is unreachable/dead
129    pub reason: String,
130}
131
132/// Cycle kind classification
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum CycleKind {
135    /// Multiple symbols calling each other (SCC with >1 member)
136    MutualRecursion,
137    /// Single symbol that calls itself (direct self-loop)
138    SelfLoop,
139}
140
141/// Cycle information for detected cycles
142///
143/// Represents a strongly connected component (SCC) with more than one member,
144/// indicating mutual recursion or a cycle in the call graph.
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Cycle {
147    /// All symbols that participate in this cycle
148    pub members: Vec<SymbolInfo>,
149    /// Classification of the cycle type
150    pub kind: CycleKind,
151}
152
153/// Cycle detection report
154///
155/// Result of running [`CodeGraph::detect_cycles()`], containing all cycles
156/// found in the call graph.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct CycleReport {
159    /// All detected cycles
160    pub cycles: Vec<Cycle>,
161    /// Total number of cycles found
162    pub total_count: usize,
163}
164
165/// Supernode in a condensation graph
166///
167/// Represents an SCC collapsed into a single node for DAG analysis.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct Supernode {
170    /// Supernode ID (stable identifier for this SCC)
171    pub id: i64,
172    /// All symbols that are members of this SCC/supernode
173    pub members: Vec<SymbolInfo>,
174}
175
176/// Condensation graph (DAG after SCC collapse)
177///
178/// Represents the call graph after collapsing all SCCs into supernodes.
179/// The condensation graph is always a DAG (no cycles).
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct CondensationGraph {
182    /// All supernodes in the condensed graph
183    pub supernodes: Vec<Supernode>,
184    /// Edges between supernodes (from_supernode_id, to_supernode_id)
185    pub edges: Vec<(i64, i64)>,
186}
187
188/// Condensation result with symbol-to-supernode mapping
189///
190/// Result of running [`CodeGraph::condense_call_graph()`], providing
191/// both the condensed DAG and the mapping from original symbols to supernodes.
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct CondensationResult {
194    /// The condensed DAG
195    pub graph: CondensationGraph,
196    /// Maps symbol_id to the supernode ID containing that symbol
197    pub original_to_supernode: HashMap<String, i64>,
198}
199
200/// Direction of program slicing
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum SliceDirection {
203    /// Backward slice: what affects this symbol (reverse reachability)
204    Backward,
205    /// Forward slice: what this symbol affects (forward reachability)
206    Forward,
207}
208
209/// Program slice result
210///
211/// Contains the slice results and statistics for a program slicing operation.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct ProgramSlice {
214    /// Target symbol for the slice
215    pub target: SymbolInfo,
216    /// Direction of the slice
217    pub direction: SliceDirection,
218    /// Symbols included in the slice
219    pub included_symbols: Vec<SymbolInfo>,
220    /// Number of symbols in the slice
221    pub symbol_count: usize,
222}
223
224/// Program slice result with statistics
225///
226/// Wraps a [`ProgramSlice`] with additional statistics about the slice.
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct SliceResult {
229    /// The slice itself
230    pub slice: ProgramSlice,
231    /// Statistics about the slice
232    pub statistics: SliceStatistics,
233}
234
235/// Statistics for a program slice
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub struct SliceStatistics {
238    /// Total number of symbols in the slice
239    pub total_symbols: usize,
240    /// Number of data dependencies
241    /// Note: Set to 0 for call-graph fallback (not computed without full CFG)
242    pub data_dependencies: usize,
243    /// Number of control dependencies
244    /// For call-graph fallback, this equals total_symbols (callers/callees)
245    pub control_dependencies: usize,
246}
247
248/// Execution path in the call graph
249///
250/// Represents a single path through the call graph from a starting symbol
251/// to an ending symbol, with metadata about the path.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ExecutionPath {
254    /// Symbols along the path in order from start to end
255    pub symbols: Vec<SymbolInfo>,
256    /// Number of symbols in the path
257    pub length: usize,
258}
259
260/// Path enumeration result
261///
262/// Contains all discovered execution paths and statistics about the enumeration.
263#[derive(Debug, Clone)]
264pub struct PathEnumerationResult {
265    /// All discovered paths
266    pub paths: Vec<ExecutionPath>,
267    /// Total number of paths enumerated
268    pub total_enumerated: usize,
269    /// Whether enumeration was cut off due to bounds
270    pub bounded_hit: bool,
271    /// Statistics about the discovered paths
272    pub statistics: PathStatistics,
273}
274
275/// Statistics for path enumeration
276#[derive(Debug, Clone)]
277pub struct PathStatistics {
278    /// Average path length
279    pub avg_length: f64,
280    /// Minimum path length
281    pub min_length: usize,
282    /// Maximum path length
283    pub max_length: usize,
284    /// Number of unique symbols across all paths
285    pub unique_symbols: usize,
286}
287
288impl CodeGraph {
289    /// Resolve a stable symbol ID or FQN to its entity ID
290    ///
291    /// First tries to lookup by symbol_id (32-char BLAKE3 hash).
292    /// If not found, falls back to FQN lookup for convenience.
293    ///
294    /// # Arguments
295    /// * `symbol_id_or_fqn` - Stable symbol ID (32-char BLAKE3 hash) or FQN
296    ///
297    /// # Returns
298    /// The entity ID (i64 database row ID) for the symbol
299    ///
300    /// # Errors
301    /// Returns an error if the symbol is not found in the database
302    fn resolve_symbol_entity(&self, symbol_id_or_fqn: &str) -> Result<i64> {
303        let conn = self.chunks.connect()?;
304
305        // First try: lookup by symbol_id
306        let mut stmt = conn
307            .prepare_cached(
308                "SELECT id FROM graph_entities
309                 WHERE kind = 'Symbol'
310                 AND json_extract(data, '$.symbol_id') = ?1",
311            )
312            .map_err(|e| anyhow::anyhow!("Failed to prepare symbol ID query: {}", e))?;
313
314        let result = stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0));
315
316        match result {
317            Ok(entity_id) => return Ok(entity_id),
318            Err(rusqlite::Error::QueryReturnedNoRows) => {
319                // Fallback: try FQN lookup
320            }
321            Err(e) => {
322                return Err(anyhow::anyhow!("Failed to query symbol ID: {}", e));
323            }
324        }
325
326        // Fallback: lookup by FQN or display_fqn
327        let mut stmt = conn
328            .prepare_cached(
329                "SELECT id FROM graph_entities
330                 WHERE kind = 'Symbol'
331                 AND (json_extract(data, '$.fqn') = ?1
332                      OR json_extract(data, '$.display_fqn') = ?1
333                      OR json_extract(data, '$.canonical_fqn') = ?1)",
334            )
335            .map_err(|e| anyhow::anyhow!("Failed to prepare FQN query: {}", e))?;
336
337        stmt.query_row(params![symbol_id_or_fqn], |row| row.get::<_, i64>(0))
338            .map_err(|e| match e {
339                rusqlite::Error::QueryReturnedNoRows => {
340                    anyhow::anyhow!(
341                        "Symbol '{}' not found in database (tried symbol_id, fqn, display_fqn, canonical_fqn)",
342                        symbol_id_or_fqn
343                    )
344                }
345                _ => anyhow::anyhow!("Failed to query symbol by FQN: {}", e),
346            })
347    }
348
349    /// Get symbol information by entity ID
350    ///
351    /// # Arguments
352    /// * `entity_id` - Entity ID (i64 database row ID)
353    ///
354    /// # Returns
355    /// SymbolInfo with metadata from the symbol node
356    pub fn symbol_by_entity_id(&self, entity_id: i64) -> Result<SymbolInfo> {
357        let snapshot = SnapshotId::current();
358        let node = self
359            .calls
360            .backend
361            .get_node(snapshot, entity_id)
362            .map_err(|e| anyhow::anyhow!("Failed to get entity {}: {}", entity_id, e))?;
363
364        if node.kind != "Symbol" {
365            return Err(anyhow::anyhow!(
366                "Entity {} is not a Symbol (kind: {})",
367                entity_id,
368                node.kind
369            ));
370        }
371
372        let symbol_node: SymbolNode = serde_json::from_value(node.data)
373            .map_err(|e| anyhow::anyhow!("Failed to parse SymbolNode data: {}", e))?;
374
375        Ok(SymbolInfo {
376            symbol_id: symbol_node.symbol_id,
377            fqn: symbol_node.fqn.or_else(|| symbol_node.display_fqn.clone()),
378            file_path: node
379                .file_path
380                .unwrap_or_else(|| "?".to_string()),
381            kind: symbol_node.kind,
382        })
383    }
384
385    /// Get all Symbol entity IDs in the call graph
386    ///
387    /// Returns entity IDs for all Symbol nodes that are part of the call graph
388    /// (have incoming or outgoing CALLS edges).
389    ///
390    /// # Returns
391    /// Vector of entity IDs for call graph symbols
392    fn all_call_graph_entities(&self) -> Result<Vec<i64>> {
393        let conn = self.chunks.connect()?;
394
395        // Find all symbols that participate in CALLS edges
396        // (either as caller via CALLER edges or as callee via CALLS edges)
397        let mut stmt = conn
398            .prepare_cached(
399                "SELECT DISTINCT from_id FROM graph_edges WHERE edge_type = 'CALLER'
400                 UNION
401                 SELECT DISTINCT to_id FROM graph_edges WHERE edge_type = 'CALLS'",
402            )
403            .map_err(|e| anyhow::anyhow!("Failed to prepare call graph query: {}", e))?;
404
405        let entity_ids = stmt
406            .query_map([], |row| row.get::<_, i64>(0))
407            .map_err(|e| anyhow::anyhow!("Failed to execute call graph query: {}", e))?
408            .collect::<Result<Vec<_>, _>>()
409            .map_err(|e| anyhow::anyhow!("Failed to collect call graph results: {}", e))?;
410
411        Ok(entity_ids)
412    }
413
414    /// Find all symbols reachable from a given symbol (forward reachability)
415    ///
416    /// Computes the transitive closure of the call graph starting from the
417    /// specified symbol. Returns all symbols that can be reached by following
418    /// CALLS edges from the starting symbol.
419    ///
420    /// # Graph View
421    ///
422    /// This operates on the **call graph** (CALLS edges only), not other edge types.
423    /// The starting symbol itself is NOT included in the results.
424    ///
425    /// # Arguments
426    /// * `symbol_id` - Stable symbol ID to start from (or FQN as fallback)
427    /// * `max_depth` - Optional maximum depth limit (None = unlimited)
428    ///
429    /// # Returns
430    /// Vector of [`SymbolInfo`] for reachable symbols, sorted deterministically
431    ///
432    /// # Example
433    ///
434    /// \`\`\`no_run
435    /// # use magellan::CodeGraph;
436    /// # let mut graph = CodeGraph::open("test.db").unwrap();
437    /// // Find all functions called from main (directly or indirectly)
438    /// let reachable = graph.reachable_symbols("main", None)?;
439    /// \`\`\`
440    pub fn reachable_symbols(
441        &self,
442        symbol_id: &str,
443        _max_depth: Option<usize>,
444    ) -> Result<Vec<SymbolInfo>> {
445        let entity_id = self.resolve_symbol_entity(symbol_id)?;
446        let backend = &self.calls.backend;
447
448        // Use sqlitegraph's reachable_from algorithm
449        // This traverses outgoing edges from the start node
450        // Note: sqlitegraph 1.3.0 API takes (graph, start), not (backend, snapshot, start, depth)
451        let reachable_entity_ids = algo::reachable_from(get_sqlite_graph(backend)?, entity_id)?;
452
453        // Convert entity IDs to SymbolInfo
454        let mut symbols = Vec::new();
455        for id in reachable_entity_ids {
456            // Skip the starting symbol itself
457            if id == entity_id {
458                continue;
459            }
460
461            if let Ok(info) = self.symbol_by_entity_id(id) {
462                symbols.push(info);
463            }
464        }
465
466        // Sort deterministically for stable output
467        symbols.sort_by(|a, b| {
468            a.file_path
469                .cmp(&b.file_path)
470                .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
471                .then_with(|| a.kind.cmp(&b.kind))
472        });
473
474        Ok(symbols)
475    }
476
477    /// Find all symbols that can reach a given symbol (reverse reachability)
478    ///
479    /// Computes the reverse transitive closure of the call graph. Returns all
480    /// symbols from which the specified symbol can be reached by following
481    /// CALLS edges (i.e., all callers that directly or indirectly call this symbol).
482    ///
483    /// # Graph View
484    ///
485    /// This operates on the **call graph** (CALLS edges only).
486    ///
487    /// # Arguments
488    /// * `symbol_id` - Stable symbol ID to analyze
489    /// * `max_depth` - Optional maximum depth limit (None = unlimited)
490    ///
491    /// # Returns
492    /// Vector of [`SymbolInfo`] for symbols that can reach the target, sorted deterministically
493    ///
494    /// # Example
495    ///
496    /// \`\`\`no_run
497    /// # use magellan::CodeGraph;
498    /// # let mut graph = CodeGraph::open("test.db").unwrap();
499    /// // Find all functions that directly or indirectly call 'helper_function'
500    /// let callers = graph.reverse_reachable_symbols("helper_symbol_id", None)?;
501    /// \`\`\`
502    pub fn reverse_reachable_symbols(
503        &self,
504        symbol_id: &str,
505        _max_depth: Option<usize>,
506    ) -> Result<Vec<SymbolInfo>> {
507        let entity_id = self.resolve_symbol_entity(symbol_id)?;
508        let backend = &self.calls.backend;
509
510        // Use sqlitegraph's reverse_reachable_from algorithm
511        // This traverses incoming edges to the target node
512        // Note: sqlitegraph 1.3.0 API takes (graph, target)
513        let reachable_entity_ids = algo::reverse_reachable_from(get_sqlite_graph(backend)?, entity_id)?;
514
515        // Convert entity IDs to SymbolInfo
516        let mut symbols = Vec::new();
517        for id in reachable_entity_ids {
518            // Skip the starting symbol itself
519            if id == entity_id {
520                continue;
521            }
522
523            if let Ok(info) = self.symbol_by_entity_id(id) {
524                symbols.push(info);
525            }
526        }
527
528        // Sort deterministically for stable output
529        symbols.sort_by(|a, b| {
530            a.file_path
531                .cmp(&b.file_path)
532                .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
533                .then_with(|| a.kind.cmp(&b.kind))
534        });
535
536        Ok(symbols)
537    }
538
539    /// Find dead code unreachable from an entry point symbol
540    ///
541    /// Identifies all symbols in the call graph that cannot be reached from
542    /// the specified entry point (e.g., `main`, `test_main`). This is useful
543    /// for detecting unused functions and dead code.
544    ///
545    /// # Graph View
546    ///
547    /// This operates on the **call graph** (CALLS edges only). Symbols not
548    /// participating in call edges are not considered.
549    ///
550    /// # Limitations
551    ///
552    /// - Only considers the call graph. Symbols called via reflection,
553    ///   function pointers, or dynamic dispatch may be incorrectly flagged.
554    /// - Test functions, benchmark code, and platform-specific code may
555    ///   appear as dead code if not reachable from the specified entry point.
556    ///
557    /// # Arguments
558    /// * `entry_symbol_id` - Stable symbol ID of the entry point (e.g., main function)
559    ///
560    /// # Returns
561    /// Vector of [`DeadSymbol`] for unreachable symbols, sorted deterministically
562    ///
563    /// # Example
564    ///
565    /// \`\`\`no_run
566    /// # use magellan::CodeGraph;
567    /// # let mut graph = CodeGraph::open("test.db").unwrap();
568    /// // Find all functions unreachable from main
569    /// let dead = graph.dead_symbols("main_symbol_id")?;
570    /// for dead_symbol in dead {
571    ///     println!("Dead: {} in {} ({})",
572    ///         dead_symbol.symbol.fqn.as_deref().unwrap_or("?"),
573    ///         dead_symbol.symbol.file_path,
574    ///         dead_symbol.reason);
575    /// }
576    /// \`\`\`
577    pub fn dead_symbols(&self, entry_symbol_id: &str) -> Result<Vec<DeadSymbol>> {
578        let entry_entity = self.resolve_symbol_entity(entry_symbol_id)?;
579        let backend = &self.calls.backend;
580
581        // Get all call graph entities
582        let all_entities = self.all_call_graph_entities()?;
583
584        // Find all entities reachable from the entry point
585        // Note: sqlitegraph 1.3.0 API takes (graph, start)
586        let reachable_ids =
587            algo::reachable_from(get_sqlite_graph(backend)?, entry_entity)?;
588
589        // Dead symbols = all entities - reachable entities
590        let reachable_set: HashSet<i64> = reachable_ids.into_iter().collect();
591        let mut dead_symbols = Vec::new();
592
593        for entity_id in all_entities {
594            // Skip the entry point itself
595            if entity_id == entry_entity {
596                continue;
597            }
598
599            // If not reachable from entry point, it's dead code
600            if !reachable_set.contains(&entity_id) {
601                if let Ok(info) = self.symbol_by_entity_id(entity_id) {
602                    dead_symbols.push(DeadSymbol {
603                        reason: "unreachable from entry point".to_string(),
604                        symbol: info,
605                    });
606                }
607            }
608        }
609
610        // Sort deterministically for stable output
611        dead_symbols.sort_by(|a, b| {
612            a.symbol
613                .file_path
614                .cmp(&b.symbol.file_path)
615                .then_with(|| a.symbol.fqn.as_ref().cmp(&b.symbol.fqn.as_ref()))
616                .then_with(|| a.symbol.kind.cmp(&b.symbol.kind))
617        });
618
619        Ok(dead_symbols)
620    }
621
622    /// Detect cycles in the call graph using SCC decomposition
623    ///
624    /// Finds all strongly connected components (SCCs) with more than one member,
625    /// which indicate cycles or mutual recursion in the call graph.
626    ///
627    /// # Graph View
628    ///
629    /// This operates on the **call graph** (CALLS edges only).
630    ///
631    /// # Cycle Detection
632    ///
633    /// Uses Tarjan's SCC algorithm to find strongly connected components.
634    /// Only SCCs with more than one member are reported as cycles (MutualRecursion).
635    /// Single-node SCCs are not cycles (unless they have self-loops).
636    ///
637    /// # Returns
638    /// [`CycleReport`] containing all detected cycles
639    ///
640    /// # Example
641    ///
642    /// \`\`\`no_run
643    /// # use magellan::CodeGraph;
644    /// # let mut graph = CodeGraph::open("test.db").unwrap();
645    /// let report = graph.detect_cycles()?;
646    /// println!("Found {} cycles", report.total_count);
647    /// for cycle in &report.cycles {
648    ///     println!("Cycle with {} members:", cycle.members.len());
649    ///     for member in &cycle.members {
650    ///         println!("  - {}", member.fqn.as_deref().unwrap_or("?"));
651    ///     }
652    /// }
653    /// \`\`\`
654    pub fn detect_cycles(&self) -> Result<CycleReport> {
655        let backend = &self.calls.backend;
656
657        // Use sqlitegraph's strongly_connected_components algorithm
658        let scc_result = algo::strongly_connected_components(get_sqlite_graph(backend)?)?;
659
660        // Filter to SCCs with >1 member (mutual recursion)
661        let cycles: Vec<_> = scc_result
662            .components
663            .into_iter()
664            .filter(|scc| scc.len() > 1)
665            .map(|members| {
666                // Convert entity IDs to SymbolInfo
667                let symbol_infos: Vec<_> = members
668                    .into_iter()
669                    .filter_map(|id| self.symbol_by_entity_id(id).ok())
670                    .collect();
671
672                Cycle {
673                    members: symbol_infos,
674                    kind: CycleKind::MutualRecursion,
675                }
676            })
677            .filter(|cycle| !cycle.members.is_empty())
678            .collect();
679
680        let total_count = cycles.len();
681
682        // Sort cycles deterministically
683        let mut cycles = cycles;
684        cycles.sort_by(|a, b| {
685            match (a.members.first(), b.members.first()) {
686                (Some(am), Some(bm)) => {
687                    match (am.fqn.as_ref(), bm.fqn.as_ref()) {
688                        (Some(af), Some(bf)) => af.cmp(bf),
689                        (Some(_), None) => std::cmp::Ordering::Less,
690                        (None, Some(_)) => std::cmp::Ordering::Greater,
691                        (None, None) => std::cmp::Ordering::Equal,
692                    }
693                }
694                (Some(_), None) => std::cmp::Ordering::Less,
695                (None, Some(_)) => std::cmp::Ordering::Greater,
696                (None, None) => std::cmp::Ordering::Equal,
697            }
698        });
699
700        Ok(CycleReport {
701            cycles,
702            total_count,
703        })
704    }
705
706    /// Find cycles containing a specific symbol
707    ///
708    /// Returns only the cycles that include the specified symbol in their member set.
709    ///
710    /// # Arguments
711    /// * `symbol_id` - Stable symbol ID or FQN to search for
712    ///
713    /// # Returns
714    /// Vector of [`Cycle`] containing the specified symbol
715    ///
716    /// # Example
717    ///
718    /// \`\`\`no_run
719    /// # use magellan::CodeGraph;
720    /// # let mut graph = CodeGraph::open("test.db").unwrap();
721    /// let cycles = graph.find_cycles_containing("problematic_function")?;
722    /// if cycles.is_empty() {
723    ///     println!("No cycles found containing this symbol");
724    /// } else {
725    ///     println!("Found {} cycles containing this symbol", cycles.len());
726    /// }
727    /// \`\`\`
728    pub fn find_cycles_containing(&self, symbol_id: &str) -> Result<Vec<Cycle>> {
729        let entity_id = self.resolve_symbol_entity(symbol_id)?;
730        let backend = &self.calls.backend;
731
732        // Use sqlitegraph's strongly_connected_components algorithm
733        let scc_result = algo::strongly_connected_components(get_sqlite_graph(backend)?)?;
734
735        // Find which SCC contains this entity
736        let target_component_idx = scc_result.node_to_component.get(&entity_id);
737
738        let target_idx = match target_component_idx {
739            Some(&idx) => idx,
740            None => return Ok(Vec::new()), // Symbol not in any SCC (shouldn't happen)
741        };
742
743        // Check if this SCC is a cycle (has >1 member)
744        let target_component = &scc_result.components[target_idx];
745        if target_component.len() <= 1 {
746            // Single node SCC - not a cycle (unless self-loop, but that's rare)
747            return Ok(Vec::new());
748        }
749
750        // Convert this SCC to a Cycle
751        let symbol_infos: Vec<_> = target_component
752            .iter()
753            .filter_map(|&id| self.symbol_by_entity_id(id).ok())
754            .collect();
755
756        let cycle = Cycle {
757            members: symbol_infos,
758            kind: CycleKind::MutualRecursion,
759        };
760
761        Ok(vec![cycle])
762    }
763
764    /// Condense the call graph by collapsing SCCs into supernodes
765    ///
766    /// Creates a condensation DAG by collapsing each strongly connected component
767    /// into a single "supernode". The resulting graph is always acyclic, making it
768    /// suitable for topological analysis and safe refactoring.
769    ///
770    /// # Graph View
771    ///
772    /// This operates on the **call graph** (CALLS edges only).
773    ///
774    /// # Use Cases
775    ///
776    /// - **Topological Sorting**: Condensation graph is a DAG, enabling topo sort
777    /// - **Mutual Recursion Detection**: Large supernodes indicate tight coupling
778    /// - **Impact Analysis**: Changing one symbol affects its entire SCC
779    ///
780    /// # Returns
781    /// [`CondensationResult`] with the condensed DAG and symbol-to-supernode mapping
782    ///
783    /// # Example
784    ///
785    /// \`\`\`no_run
786    /// # use magellan::CodeGraph;
787    /// # let mut graph = CodeGraph::open("test.db").unwrap();
788    /// let condensed = graph.condense_call_graph()?;
789    ///
790    /// println!("Condensed to {} supernodes", condensed.graph.supernodes.len());
791    /// println!("Condensed graph has {} edges", condensed.graph.edges.len());
792    ///
793    /// // Check which SCC a symbol belongs to
794    /// if let Some(supernode_id) = condensed.original_to_supernode.get("some_symbol_id") {
795    ///     println!("Symbol is in SCC {}", supernode_id);
796    /// }
797    /// \`\`\`
798    pub fn condense_call_graph(&self) -> Result<CondensationResult> {
799        let backend = &self.calls.backend;
800
801        // Use sqlitegraph's collapse_sccs algorithm
802        let collapse_result = algo::collapse_sccs(get_sqlite_graph(backend)?)?;
803
804        // Build supernodes with SymbolInfo members
805        let mut supernodes = Vec::new();
806        let mut original_to_supernode = HashMap::new();
807
808        for (&supernode_id, member_ids) in &collapse_result.supernode_members {
809            let symbol_infos: Vec<_> = member_ids
810                .iter()
811                .filter_map(|&id| self.symbol_by_entity_id(id).ok())
812                .collect();
813
814            // Build mapping from symbol_id to supernode
815            for symbol_info in &symbol_infos {
816                if let Some(ref sym_id) = symbol_info.symbol_id {
817                    original_to_supernode.insert(sym_id.clone(), supernode_id);
818                }
819            }
820
821            supernodes.push(Supernode {
822                id: supernode_id,
823                members: symbol_infos,
824            });
825        }
826
827        // Sort supernodes deterministically
828        supernodes.sort_by(|a, b| a.id.cmp(&b.id));
829
830        let graph = CondensationGraph {
831            supernodes,
832            edges: collapse_result.supernode_edges,
833        };
834
835        Ok(CondensationResult {
836            graph,
837            original_to_supernode,
838        })
839    }
840
841    /// Compute a backward program slice (what affects this symbol)
842    ///
843    /// Returns all symbols that can affect the target symbol through the call graph.
844    /// This is useful for bug isolation - finding all code that could influence
845    /// a given symbol's behavior.
846    ///
847    /// # Graph View (Call-Graph Fallback)
848    ///
849    /// **Current implementation uses call-graph reachability as a fallback.**
850    /// Full CFG-based program slicing requires control dependence graph (CDG)
851    /// which needs post-dominators and AST CFG integration not yet available.
852    ///
853    /// The fallback implementation uses reverse reachability on the call graph,
854    /// finding all callers that directly or indirectly call this symbol.
855    ///
856    /// # Limitations
857    ///
858    /// - Uses call-graph reachability instead of full CFG-based slicing
859    /// - Does not include data flow dependencies within functions
860    /// - Does not include control flow from conditionals/loops
861    /// - Full slicing will be available when AST CFG edges are integrated
862    ///
863    /// # Arguments
864    /// * `symbol_id` - Stable symbol ID or FQN to slice from
865    ///
866    /// # Returns
867    /// [`SliceResult`] containing the slice and statistics
868    ///
869    /// # Example
870    ///
871    /// \`\`\`no_run
872    /// # use magellan::CodeGraph;
873    /// # let mut graph = CodeGraph::open("test.db").unwrap();
874    /// // Find what affects 'helper_function'
875    /// let slice_result = graph.backward_slice("helper_function")?;
876    /// println!("{} symbols affect this function", slice_result.slice.symbol_count);
877    /// for symbol in &slice_result.slice.included_symbols {
878    ///     println!("  - {}", symbol.fqn.as_deref().unwrap_or("?"));
879    /// }
880    /// \`\`\`
881    pub fn backward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
882        let entity_id = self.resolve_symbol_entity(symbol_id)?;
883        let backend = &self.calls.backend;
884
885        // Get target symbol info
886        let target = self.symbol_by_entity_id(entity_id)?;
887
888        // Fallback: Use reverse reachable on call graph
889        // This finds all callers that directly or indirectly call this symbol
890        let caller_entity_ids = algo::reverse_reachable_from(get_sqlite_graph(backend)?, entity_id)?;
891
892        // Convert entity IDs to SymbolInfo
893        let mut included_symbols = Vec::new();
894        for id in caller_entity_ids {
895            // Skip the starting symbol itself
896            if id == entity_id {
897                continue;
898            }
899
900            if let Ok(info) = self.symbol_by_entity_id(id) {
901                included_symbols.push(info);
902            }
903        }
904
905        // Sort deterministically
906        included_symbols.sort_by(|a, b| {
907            a.file_path
908                .cmp(&b.file_path)
909                .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
910                .then_with(|| a.kind.cmp(&b.kind))
911        });
912
913        let symbol_count = included_symbols.len();
914
915        Ok(SliceResult {
916            slice: ProgramSlice {
917                target,
918                direction: SliceDirection::Backward,
919                included_symbols,
920                symbol_count,
921            },
922            statistics: SliceStatistics {
923                total_symbols: symbol_count,
924                data_dependencies: 0, // Not available in call-graph fallback
925                control_dependencies: symbol_count,
926            },
927        })
928    }
929
930    /// Compute a forward program slice (what this symbol affects)
931    ///
932    /// Returns all symbols that the target symbol can affect through the call graph.
933    /// This is useful for refactoring safety - finding all code that could be
934    /// impacted by changes to this symbol.
935    ///
936    /// # Graph View (Call-Graph Fallback)
937    ///
938    /// **Current implementation uses call-graph reachability as a fallback.**
939    /// Full CFG-based program slicing requires control dependence graph (CDG)
940    /// which needs post-dominators and AST CFG integration not yet available.
941    ///
942    /// The fallback implementation uses forward reachability on the call graph,
943    /// finding all callees that this symbol directly or indirectly calls.
944    ///
945    /// # Limitations
946    ///
947    /// - Uses call-graph reachability instead of full CFG-based slicing
948    /// - Does not include data flow dependencies within functions
949    /// - Does not include control flow from conditionals/loops
950    /// - Full slicing will be available when AST CFG edges are integrated
951    ///
952    /// # Arguments
953    /// * `symbol_id` - Stable symbol ID or FQN to slice from
954    ///
955    /// # Returns
956    /// [`SliceResult`] containing the slice and statistics
957    ///
958    /// # Example
959    ///
960    /// \`\`\`no_run
961    /// # use magellan::CodeGraph;
962    /// # let mut graph = CodeGraph::open("test.db").unwrap();
963    /// // Find what 'main_function' affects
964    /// let slice_result = graph.forward_slice("main_function")?;
965    /// println!("{} symbols are affected by this function", slice_result.slice.symbol_count);
966    /// for symbol in &slice_result.slice.included_symbols {
967    ///     println!("  - {}", symbol.fqn.as_deref().unwrap_or("?"));
968    /// }
969    /// \`\`\`
970    pub fn forward_slice(&self, symbol_id: &str) -> Result<SliceResult> {
971        let entity_id = self.resolve_symbol_entity(symbol_id)?;
972        let backend = &self.calls.backend;
973
974        // Get target symbol info
975        let target = self.symbol_by_entity_id(entity_id)?;
976
977        // Fallback: Use forward reachable on call graph
978        // This finds all callees that this symbol directly or indirectly calls
979        let callee_entity_ids = algo::reachable_from(get_sqlite_graph(backend)?, entity_id)?;
980
981        // Convert entity IDs to SymbolInfo
982        let mut included_symbols = Vec::new();
983        for id in callee_entity_ids {
984            // Skip the starting symbol itself
985            if id == entity_id {
986                continue;
987            }
988
989            if let Ok(info) = self.symbol_by_entity_id(id) {
990                included_symbols.push(info);
991            }
992        }
993
994        // Sort deterministically
995        included_symbols.sort_by(|a, b| {
996            a.file_path
997                .cmp(&b.file_path)
998                .then_with(|| a.fqn.as_ref().cmp(&b.fqn.as_ref()))
999                .then_with(|| a.kind.cmp(&b.kind))
1000        });
1001
1002        let symbol_count = included_symbols.len();
1003
1004        Ok(SliceResult {
1005            slice: ProgramSlice {
1006                target,
1007                direction: SliceDirection::Forward,
1008                included_symbols,
1009                symbol_count,
1010            },
1011            statistics: SliceStatistics {
1012                total_symbols: symbol_count,
1013                data_dependencies: 0, // Not available in call-graph fallback
1014                control_dependencies: symbol_count,
1015            },
1016        })
1017    }
1018
1019    /// Enumerate execution paths from a starting symbol
1020    ///
1021    /// Finds all execution paths from `start_symbol_id` to `end_symbol_id` (if provided)
1022    /// or all paths starting from `start_symbol_id` (if end_symbol_id is None).
1023    ///
1024    /// Path enumeration uses bounded DFS to prevent infinite traversal in cyclic graphs:
1025    /// - `max_depth`: Maximum path length (number of edges)
1026    /// - `max_paths`: Maximum number of paths to return
1027    /// - `revisit_cap`: Maximum number of times a single node can be revisited (prevents infinite loops)
1028    ///
1029    /// # Arguments
1030    ///
1031    /// * `start_symbol_id` - Starting symbol ID or FQN
1032    /// * `end_symbol_id` - Optional ending symbol ID or FQN (if None, enumerates all paths from start)
1033    /// * `max_depth` - Maximum path depth (default: 100)
1034    /// * `max_paths` - Maximum number of paths to return (default: 1000)
1035    ///
1036    /// # Returns
1037    ///
1038    /// Returns a [`PathEnumerationResult`] containing:
1039    /// - All discovered paths
1040    /// - Whether enumeration hit bounds
1041    /// - Statistics about path lengths and unique symbols
1042    ///
1043    /// # Example
1044    ///
1045    /// \`\`\`no_run
1046    /// # use magellan::CodeGraph;
1047    /// # fn main() -> anyhow::Result<()> {
1048    /// let mut graph = CodeGraph::open("codegraph.db")?;
1049    ///
1050    /// // Find all paths from main to any leaf function
1051    /// let result = graph.enumerate_paths("main", None, 50, 100)?;
1052    ///
1053    /// println!("Found {} paths", result.total_enumerated);
1054    /// println!("Average length: {:.2}", result.statistics.avg_length);
1055    /// for (i, path) in result.paths.iter().enumerate() {
1056    ///     println!("Path {}: {:?}", path.symbols.iter().map(|s| s.fqn.as_deref().unwrap_or("?")).collect::<Vec<_>>());
1057    /// }
1058    /// # Ok(())
1059    /// # }
1060    /// \`\`\`
1061    pub fn enumerate_paths(
1062        &self,
1063        start_symbol_id: &str,
1064        end_symbol_id: Option<&str>,
1065        max_depth: usize,
1066        max_paths: usize,
1067    ) -> Result<PathEnumerationResult> {
1068        let start_entity_id = self.resolve_symbol_entity(start_symbol_id)?;
1069        let backend = &self.calls.backend;
1070        let graph = get_sqlite_graph(backend)?;
1071
1072        // Build exit_nodes set for target symbol
1073        let exit_nodes: Option<AHashSet<i64>> = if let Some(end_id) = end_symbol_id {
1074            let end_entity_id = self.resolve_symbol_entity(end_id)?;
1075            let mut set = AHashSet::new();
1076            set.insert(end_entity_id);
1077            Some(set)
1078        } else {
1079            None
1080        };
1081
1082        // Use sqlitegraph's path enumeration with bounds
1083        let config = algo::PathEnumerationConfig {
1084            max_depth,
1085            max_paths,
1086            revisit_cap: 100, // Prevent infinite loops in cyclic graphs
1087            exit_nodes,
1088            error_nodes: None,
1089        };
1090
1091        let sqlite_result = algo::enumerate_paths(graph, start_entity_id, &config)?;
1092
1093        // Convert sqlitegraph's path result to our format
1094        let mut paths = Vec::new();
1095        let mut all_symbols = HashSet::new();
1096        let mut min_length = usize::MAX;
1097        let mut max_length = 0;
1098        let mut total_length = 0;
1099
1100        for path in sqlite_result.paths {
1101            let mut symbols = Vec::new();
1102
1103            for entity_id in &path.nodes {
1104                if let Ok(info) = self.symbol_by_entity_id(*entity_id) {
1105                    all_symbols.insert(info.symbol_id.clone().unwrap_or_default());
1106                    symbols.push(info);
1107                }
1108            }
1109
1110            let length = symbols.len();
1111            if length > 0 {
1112                min_length = min_length.min(length);
1113                max_length = max_length.max(length);
1114                total_length += length;
1115
1116                paths.push(ExecutionPath {
1117                    symbols,
1118                    length,
1119                });
1120            }
1121        }
1122
1123        // Sort paths: first by starting symbol FQN, then by length
1124        paths.sort_by(|a, b| {
1125            match (
1126                a.symbols.first().and_then(|s| s.fqn.as_ref()),
1127                b.symbols.first().and_then(|s| s.fqn.as_ref()),
1128            ) {
1129                (Some(a_fqn), Some(b_fqn)) => {
1130                    a_fqn
1131                        .cmp(b_fqn)
1132                        .then_with(|| a.length.cmp(&b.length))
1133                }
1134                (Some(_), None) => std::cmp::Ordering::Less,
1135                (None, Some(_)) => std::cmp::Ordering::Greater,
1136                (None, None) => a.length.cmp(&b.length),
1137            }
1138        });
1139
1140        let avg_length = if paths.is_empty() {
1141            0.0
1142        } else {
1143            total_length as f64 / paths.len() as f64
1144        };
1145
1146        // Determine if we hit bounds
1147        let bounded_hit = sqlite_result.paths_pruned_by_bounds > 0;
1148
1149        Ok(PathEnumerationResult {
1150            paths,
1151            total_enumerated: sqlite_result.total_paths_found,
1152            bounded_hit,
1153            statistics: PathStatistics {
1154                avg_length,
1155                min_length: if min_length == usize::MAX {
1156                    0
1157                } else {
1158                    min_length
1159                },
1160                max_length,
1161                unique_symbols: all_symbols.len(),
1162            },
1163        })
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170    use crate::CodeGraph;
1171
1172    /// Test helper to create a simple call graph for testing
1173    ///
1174    /// Creates:
1175    /// - main() -> helper_a() -> leaf()
1176    /// - main() -> helper_b() -> leaf()
1177    /// - unused_function() -> leaf()
1178    ///
1179    /// Returns the CodeGraph and symbol IDs for main and unused_function
1180    fn create_test_graph() -> Result<(CodeGraph, String, String)> {
1181        // Use a persistent temp directory that won't be deleted
1182        // This is necessary for V3 backend which needs files to remain accessible
1183        let temp_dir = std::env::temp_dir().join(format!("magellan_test_{}", std::process::id()));
1184        std::fs::create_dir_all(&temp_dir)?;
1185        let db_path = temp_dir.join("test.db");
1186
1187        let source = r#"
1188fn main() {
1189    helper_a();
1190    helper_b();
1191}
1192
1193fn helper_a() {
1194    leaf();
1195}
1196
1197fn helper_b() {
1198    leaf();
1199}
1200
1201fn leaf() {
1202    println!("leaf");
1203}
1204
1205fn unused_function() {
1206    leaf();
1207}
1208"#;
1209
1210        let mut graph = CodeGraph::open(&db_path)?;
1211        // Index the file - use a temporary file path
1212        let test_file = temp_dir.join("test.rs");
1213        std::fs::write(&test_file, source)?;
1214        let path_str = test_file.to_string_lossy().to_string();
1215        let source_bytes = std::fs::read(&test_file)?;
1216
1217        // Index symbols and calls
1218        graph.index_file(&path_str, &source_bytes)?;
1219        graph.index_calls(&path_str, &source_bytes)?;
1220
1221        // Find the symbol IDs for main and unused_function
1222        let symbols = crate::graph::query::symbols_in_file(&mut graph, &path_str)?;
1223        let main_id = symbols
1224            .iter()
1225            .find(|s| s.name.as_deref() == Some("main"))
1226            .and_then(|s| s.fqn.clone())
1227            .unwrap_or_default();
1228
1229        let unused_id = symbols
1230            .iter()
1231            .find(|s| s.name.as_deref() == Some("unused_function"))
1232            .and_then(|s| s.fqn.clone())
1233            .unwrap_or_default();
1234
1235        // For testing, use the symbol's FQN directly
1236        // In a real scenario with proper SymbolId generation, we'd use that
1237        Ok((graph, main_id, unused_id))
1238    }
1239
1240    #[test]
1241    fn test_resolve_symbol_entity_not_found() {
1242        let (graph, _, _) = create_test_graph().unwrap();
1243        let result = graph.resolve_symbol_entity("nonexistent_id_123456789012");
1244        assert!(result.is_err());
1245    }
1246
1247    #[test]
1248    fn test_symbol_by_entity_id() {
1249        let (graph, _, _) = create_test_graph().unwrap();
1250
1251        // Get all entity IDs
1252        let entity_ids = graph.calls.backend.entity_ids().unwrap();
1253        let snapshot = SnapshotId::current();
1254
1255        // Find a Symbol entity
1256        for entity_id in entity_ids {
1257            if let Ok(node) = graph.calls.backend.get_node(snapshot, entity_id) {
1258                if node.kind == "Symbol" {
1259                    let info = graph.symbol_by_entity_id(entity_id);
1260                    assert!(info.is_ok());
1261                    let symbol_info = info.unwrap();
1262                    assert!(!symbol_info.file_path.is_empty());
1263                    assert!(!symbol_info.kind.is_empty());
1264                    return;
1265                }
1266            }
1267        }
1268
1269        panic!("No Symbol entity found in test graph");
1270    }
1271
1272    #[test]
1273    fn test_reachable_symbols_basic() {
1274        let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1275
1276        // Get all symbols and verify we can query them
1277        let entity_ids = graph.calls.backend.entity_ids().unwrap();
1278        eprintln!("Total entities: {}", entity_ids.len());
1279        let snapshot = SnapshotId::current();
1280        let mut found_symbols = 0;
1281
1282        for entity_id in &entity_ids {
1283            match graph.calls.backend.get_node(snapshot, *entity_id) {
1284                Ok(node) => {
1285                    eprintln!("  Entity {}: kind={}", entity_id, node.kind);
1286                    if node.kind == "Symbol" {
1287                        found_symbols += 1;
1288                    }
1289                }
1290                Err(e) => {
1291                    eprintln!("  Entity {}: ERROR getting node: {:?}", entity_id, e);
1292                }
1293            }
1294        }
1295
1296        // We should have found at least some symbols
1297        assert!(found_symbols > 0, "Should find Symbol entities in test graph, got {} symbols from {} entities", found_symbols, entity_ids.len());
1298    }
1299
1300    #[test]
1301    fn test_reachable_symbols_max_depth() {
1302        let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1303
1304        // Get the main function's entity ID
1305        let snapshot = SnapshotId::current();
1306        let entity_ids = graph.calls.backend.entity_ids().unwrap();
1307
1308        let main_entity_id = entity_ids
1309            .into_iter()
1310            .find(|&id| {
1311                if let Ok(node) = graph.calls.backend.get_node(snapshot, id) {
1312                    if let Ok(data) = serde_json::from_value::<serde_json::Value>(node.data) {
1313                        if let Some(name) = data.get("name").and_then(|v| v.as_str()) {
1314                            return name == "main";
1315                        }
1316                    }
1317                }
1318                false
1319            });
1320
1321        if let Some(entity_id) = main_entity_id {
1322            // Verify we can get the node
1323            let result = graph.calls.backend.get_node(snapshot, entity_id);
1324            assert!(result.is_ok(), "Should be able to get main node");
1325        }
1326    }
1327
1328    #[test]
1329    fn test_dead_symbols() {
1330        let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1331
1332        // Get all entity IDs
1333        let entity_ids = graph.calls.backend.entity_ids().unwrap();
1334
1335        // We should have some entities in the call graph
1336        assert!(entity_ids.len() > 0, "Should have call graph entities");
1337    }
1338
1339    #[test]
1340    fn test_reverse_reachable_symbols() {
1341        let (graph, _main_id, _unused_id) = create_test_graph().unwrap();
1342
1343        // Get all entity IDs
1344        let entity_ids = graph.calls.backend.entity_ids().unwrap();
1345
1346        // We should have some entities in the call graph
1347        assert!(entity_ids.len() > 0, "Should have call graph entities");
1348    }
1349}