Skip to main content

mirage/analysis/
mod.rs

1//! Inter-procedural analysis using Magellan's call graph
2//!
3//! This module provides a bridge to Magellan's graph algorithms,
4//! enabling combined inter-procedural (Magellan) and intra-procedural (Mirage) analysis.
5//!
6//! # Architecture
7//!
8//! - **Magellan** (inter-procedural): Call graph algorithms, reachability, dead code detection
9//! - **Mirage** (intra-procedural): CFG analysis, path enumeration, dominance
10//!
11//! The [`MagellanBridge`] struct wraps Magellan's [`CodeGraph`] to provide
12//! a unified API for both layers of analysis.
13
14use anyhow::Result;
15use serde::Serialize;
16use std::collections::HashMap;
17
18// Re-export key types from Magellan for convenience
19pub use magellan::CodeGraph;
20
21// Re-export algorithm result types for ergonomic API
22pub use magellan::{
23    CondensationResult, Cycle, CycleKind, CycleReport, DeadSymbol, ExecutionPath,
24    PathEnumerationResult, SymbolInfo,
25};
26
27// Private imports for test compilation (not re-exported)
28// These are used in tests but not at module level
29#[allow(unused_imports)]
30use magellan::{
31    CondensationGraph, PathStatistics, ProgramSlice, SliceDirection, SliceResult, SliceStatistics,
32    Supernode,
33};
34
35/// Serializable wrapper for [`DeadSymbol`]
36///
37/// Magellan's [`DeadSymbol`] doesn't implement Serialize, so we provide
38/// a wrapper struct that can be serialized to JSON for CLI output.
39#[derive(Debug, Clone, Serialize)]
40pub struct DeadSymbolJson {
41    /// Fully-qualified name of the dead symbol
42    pub fqn: Option<String>,
43    /// File path containing the symbol
44    pub file_path: String,
45    /// Symbol kind (Function, Method, Class, etc.)
46    pub kind: String,
47    /// Reason why this symbol is unreachable/dead
48    pub reason: String,
49}
50
51impl From<&DeadSymbol> for DeadSymbolJson {
52    fn from(dead: &DeadSymbol) -> Self {
53        Self {
54            fqn: dead.symbol.fqn.clone(),
55            file_path: dead.symbol.file_path.clone(),
56            kind: dead.symbol.kind.clone(),
57            reason: dead.reason.clone(),
58        }
59    }
60}
61
62/// Serializable wrapper for [`SymbolInfo`]
63///
64/// Magellan's [`SymbolInfo`] doesn't implement Serialize, so we provide
65/// a wrapper struct that can be serialized to JSON for CLI output.
66#[derive(Debug, Clone, Serialize)]
67pub struct SymbolInfoJson {
68    /// Stable symbol ID (32-char BLAKE3 hash)
69    pub symbol_id: Option<String>,
70    /// Fully-qualified name of the symbol
71    pub fqn: Option<String>,
72    /// File path containing the symbol
73    pub file_path: String,
74    /// Symbol kind (Function, Method, Class, etc.)
75    pub kind: String,
76}
77
78impl From<&SymbolInfo> for SymbolInfoJson {
79    fn from(symbol: &SymbolInfo) -> Self {
80        Self {
81            symbol_id: symbol.symbol_id.clone(),
82            fqn: symbol.fqn.clone(),
83            file_path: symbol.file_path.clone(),
84            kind: symbol.kind.clone(),
85        }
86    }
87}
88
89/// Serializable wrapper for program slice results
90///
91/// Magellan's [`SliceResult`] doesn't implement Serialize, so we provide
92/// a wrapper struct that can be serialized to JSON for CLI output.
93#[derive(Debug, Clone, Serialize)]
94pub struct SliceWrapper {
95    /// Target symbol for the slice
96    pub target: SymbolInfoJson,
97    /// Direction of slicing
98    pub direction: String, // "backward" or "forward"
99    /// Symbols included in the slice
100    pub included_symbols: Vec<SymbolInfoJson>,
101    /// Number of symbols in the slice
102    pub symbol_count: usize,
103    /// Statistics about the slice
104    pub statistics: SliceStats,
105}
106
107/// Statistics for program slicing
108#[derive(Debug, Clone, Serialize)]
109pub struct SliceStats {
110    pub total_symbols: usize,
111    pub data_dependencies: usize,
112    pub control_dependencies: usize,
113}
114
115impl From<&SliceResult> for SliceWrapper {
116    fn from(result: &SliceResult) -> Self {
117        let statistics = SliceStats {
118            total_symbols: result.statistics.total_symbols,
119            data_dependencies: result.statistics.data_dependencies,
120            control_dependencies: result.statistics.control_dependencies,
121        };
122
123        SliceWrapper {
124            target: (&result.slice.target).into(),
125            direction: format!("{:?}", result.slice.direction),
126            included_symbols: result
127                .slice
128                .included_symbols
129                .iter()
130                .map(|s| s.into())
131                .collect(),
132            symbol_count: result.slice.symbol_count,
133            statistics,
134        }
135    }
136}
137
138/// Serializable wrapper for inter-procedural execution paths
139///
140/// Represents a call chain from one function to another through the call graph.
141#[derive(Debug, Clone, Serialize)]
142pub struct ExecutionPathJson {
143    /// Functions in this call chain (ordered from start to end)
144    pub symbols: Vec<SymbolInfoJson>,
145    /// Path length (number of function calls)
146    pub length: usize,
147}
148
149impl From<&ExecutionPath> for ExecutionPathJson {
150    fn from(path: &ExecutionPath) -> Self {
151        ExecutionPathJson {
152            symbols: path.symbols.iter().map(|s| s.into()).collect(),
153            length: path.length,
154        }
155    }
156}
157
158/// Serializable wrapper for path enumeration results
159///
160/// Wraps Magellan's [`PathEnumerationResult`] for CLI JSON output.
161#[derive(Debug, Clone, Serialize)]
162pub struct PathEnumerationJson {
163    /// All discovered execution paths
164    pub paths: Vec<ExecutionPathJson>,
165    /// Total number of paths enumerated
166    pub total_enumerated: usize,
167    /// Whether enumeration was truncated due to limits
168    pub truncated: bool,
169    /// Statistics about enumerated paths
170    pub statistics: PathStatisticsJson,
171}
172
173/// Serializable statistics for path enumeration
174#[derive(Debug, Clone, Serialize)]
175pub struct PathStatisticsJson {
176    /// Average path length
177    pub avg_length: f64,
178    /// Maximum path length
179    pub max_length: usize,
180    /// Minimum path length
181    pub min_length: usize,
182    /// Number of unique symbols across all paths
183    pub unique_symbols: usize,
184}
185
186impl From<&PathEnumerationResult> for PathEnumerationJson {
187    fn from(result: &PathEnumerationResult) -> Self {
188        PathEnumerationJson {
189            paths: result.paths.iter().map(|p| p.into()).collect(),
190            total_enumerated: result.total_enumerated,
191            truncated: result.bounded_hit,
192            statistics: PathStatisticsJson {
193                avg_length: result.statistics.avg_length,
194                max_length: result.statistics.max_length,
195                min_length: result.statistics.min_length,
196                unique_symbols: result.statistics.unique_symbols,
197            },
198        }
199    }
200}
201
202/// Serializable wrapper for call graph condensation results
203///
204/// Magellan's [`CondensationResult`] doesn't implement Serialize,
205/// so we provide a wrapper struct for CLI JSON output.
206#[derive(Debug, Clone, Serialize)]
207pub struct CondensationJson {
208    /// Number of supernodes (SCCs) in the condensed graph
209    pub supernode_count: usize,
210    /// Number of edges between supernodes
211    pub edge_count: usize,
212    /// Supernodes with their member functions
213    pub supernodes: Vec<SupernodeJson>,
214    /// Largest SCC size (indicates tight coupling)
215    pub largest_scc_size: usize,
216}
217
218/// Serializable representation of a supernode (SCC)
219#[derive(Debug, Clone, Serialize)]
220pub struct SupernodeJson {
221    /// Supernode ID
222    pub id: String,
223    /// Number of functions in this SCC
224    pub member_count: usize,
225    /// Member function names
226    pub members: Vec<String>,
227}
228
229impl From<&CondensationResult> for CondensationJson {
230    fn from(result: &CondensationResult) -> Self {
231        let supernodes: Vec<SupernodeJson> = result
232            .graph
233            .supernodes
234            .iter()
235            .map(|sn| SupernodeJson {
236                id: sn.id.to_string(),
237                member_count: sn.members.len(),
238                members: sn.members.iter().filter_map(|m| m.fqn.clone()).collect(),
239            })
240            .collect();
241
242        let largest_scc_size = supernodes
243            .iter()
244            .map(|sn| sn.member_count)
245            .max()
246            .unwrap_or(0);
247
248        CondensationJson {
249            supernode_count: result.graph.supernodes.len(),
250            edge_count: result.graph.edges.len(),
251            supernodes,
252            largest_scc_size,
253        }
254    }
255}
256
257/// Information about a call graph cycle
258///
259/// Serializable wrapper for cycle detection results.
260#[derive(Debug, Clone, Serialize)]
261pub struct CycleInfo {
262    /// Fully-qualified names of cycle members
263    pub members: Vec<String>,
264    /// Cycle type classification
265    pub cycle_type: String,
266    /// Number of symbols in the cycle
267    pub size: usize,
268}
269
270impl From<&Cycle> for CycleInfo {
271    fn from(cycle: &Cycle) -> Self {
272        let members: Vec<String> = cycle
273            .members
274            .iter()
275            .map(|m| m.fqn.as_deref().unwrap_or("<unknown>").to_string())
276            .collect();
277
278        let cycle_type = match cycle.kind {
279            CycleKind::MutualRecursion => "MutualRecursion",
280            CycleKind::SelfLoop => "SelfLoop",
281        };
282
283        Self {
284            members,
285            cycle_type: cycle_type.to_string(),
286            size: cycle.members.len(),
287        }
288    }
289}
290
291/// Information about a natural loop within a function
292///
293/// Represents intra-procedural loop structure detected via dominance analysis.
294#[derive(Debug, Clone, Serialize)]
295pub struct LoopInfo {
296    /// Loop header block ID
297    pub header: usize,
298    /// Back edge source block ID
299    pub back_edge_from: usize,
300    /// Number of blocks in the loop body
301    pub body_size: usize,
302    /// Nesting depth (0 for outermost loops)
303    pub nesting_level: usize,
304    /// Block IDs in the loop body
305    pub body_blocks: Vec<usize>,
306}
307
308/// Combined cycle detection report
309///
310/// Combines inter-procedural (call graph SCCs) and intra-procedural (natural loops)
311/// cycle detection for complete cycle visibility.
312#[derive(Debug, Clone, Serialize)]
313pub struct EnhancedCycles {
314    /// Inter-procedural: Call graph SCCs (mutual recursion)
315    pub call_graph_cycles: Vec<CycleInfo>,
316    /// Intra-procedural: Natural loops within functions
317    pub function_loops: HashMap<String, Vec<LoopInfo>>,
318    /// Total cycle count (call graph + function loops)
319    pub total_cycles: usize,
320}
321
322/// Enhanced dead code detection combining Magellan and Mirage analysis
323///
324/// Combines inter-procedural dead code detection (uncalled functions from Magellan)
325/// with intra-procedural dead code detection (unreachable blocks within functions from Mirage).
326///
327/// # Fields
328///
329/// - `uncalled_functions`: Functions never called from entry point (Magellan)
330/// - `unreachable_blocks`: Unreachable blocks within functions (Mirage)
331/// - `total_dead_count`: Total count of dead code items
332#[derive(Debug, Clone, Serialize)]
333pub struct EnhancedDeadCode {
334    /// From Magellan: Functions never called from entry
335    pub uncalled_functions: Vec<DeadSymbolJson>,
336    /// From Mirage: Unreachable blocks within functions (function_name -> block_ids)
337    pub unreachable_blocks: HashMap<String, Vec<usize>>,
338    /// Total count of dead code items
339    pub total_dead_count: usize,
340}
341
342/// Enhanced blast zone combining call graph and CFG impact analysis
343///
344/// This struct provides a comprehensive impact analysis by combining:
345/// - **Inter-procedural impact** (call graph): Which functions are affected
346/// - **Intra-procedural impact** (CFG): Which blocks/paths are affected within functions
347#[derive(Debug, Clone, Serialize)]
348pub struct EnhancedBlastZone {
349    /// Target function/block being analyzed
350    pub target: String,
351    /// Forward: What functions this affects (via call graph)
352    pub forward_reachable: Vec<SymbolInfoJson>,
353    /// Backward: What functions affect this (reverse call graph)
354    pub backward_reachable: Vec<SymbolInfoJson>,
355    /// Intra-procedural: Path-based impact within function
356    pub path_impact: Option<PathImpactSummary>,
357}
358
359/// Summary of path-based impact within a function
360///
361/// This represents the CFG-level impact analysis for blocks within a single function.
362#[derive(Debug, Clone, Serialize)]
363pub struct PathImpactSummary {
364    /// Path ID being analyzed
365    pub path_id: Option<String>,
366    /// Path length in blocks
367    pub path_length: usize,
368    /// Block IDs affected by the path
369    pub blocks_affected: Vec<usize>,
370    /// Count of unique blocks affected
371    pub unique_blocks_count: usize,
372}
373
374/// Bridge to Magellan's inter-procedural graph algorithms
375///
376/// Wraps [`CodeGraph`] to provide access to call graph algorithms including:
377/// - Reachability analysis (forward/reverse)
378/// - Dead code detection
379/// - Cycle detection (mutual recursion)
380/// - Program slicing
381/// - Path enumeration
382///
383/// # Example
384///
385/// ```no_run
386/// use mirage_analyzer::analysis::MagellanBridge;
387///
388/// // Open existing Magellan database
389/// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
390///
391/// // Find all functions reachable from main
392/// let reachable = bridge.reachable_symbols("main")?;
393/// println!("Found {} reachable functions", reachable.len());
394///
395/// // Find dead code unreachable from entry points
396/// let dead = bridge.graph().dead_symbols("main")?;
397/// println!("Found {} dead symbols", dead.len());
398/// # Ok::<(), anyhow::Error>(())
399/// ```
400pub struct MagellanBridge {
401    /// Underlying Magellan code graph
402    graph: CodeGraph,
403}
404
405impl MagellanBridge {
406    /// Open a Magellan database for inter-procedural analysis
407    ///
408    /// # Arguments
409    ///
410    /// * `db_path` - Path to the Magellan database file (typically `codemcp/mirage.db`)
411    ///
412    /// # Returns
413    ///
414    /// A [`MagellanBridge`] instance ready for analysis
415    ///
416    /// # Example
417    ///
418    /// ```no_run
419    /// use mirage_analyzer::analysis::MagellanBridge;
420    ///
421    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
422    /// # Ok::<(), anyhow::Error>(())
423    /// ```
424    pub fn open(db_path: &str) -> Result<Self> {
425        let graph = CodeGraph::open(db_path)?;
426        Ok(Self { graph })
427    }
428
429    /// Get a reference to the underlying Magellan [`CodeGraph`]
430    ///
431    /// Provides direct access to all Magellan algorithms for advanced use cases.
432    ///
433    /// # Example
434    ///
435    /// ```no_run
436    /// use mirage_analyzer::analysis::MagellanBridge;
437    ///
438    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
439    ///
440    /// // Access full CodeGraph API
441    /// let cycles = bridge.graph().detect_cycles()?;
442    /// # Ok::<(), anyhow::Error>(())
443    /// ```
444    pub fn graph(&self) -> &CodeGraph {
445        &self.graph
446    }
447
448    /// Find all symbols reachable from a given symbol (forward reachability)
449    ///
450    /// Computes the transitive closure of the call graph starting from the
451    /// specified symbol. This is useful for:
452    /// - Impact analysis (what does changing this symbol affect?)
453    /// - Test coverage (what code does this test exercise?)
454    /// - Dependency tracing
455    ///
456    /// # Arguments
457    ///
458    /// * `symbol_id` - Stable symbol ID (32-char BLAKE3 hash) or FQN
459    ///
460    /// # Returns
461    ///
462    /// Vector of [`SymbolInfo`] for reachable symbols, sorted deterministically
463    ///
464    /// # Example
465    ///
466    /// ```no_run
467    /// use mirage_analyzer::analysis::MagellanBridge;
468    ///
469    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
470    ///
471    /// // Find all functions called from main (directly or indirectly)
472    /// let reachable = bridge.reachable_symbols("main")?;
473    /// for symbol in reachable {
474    ///     println!("  - {}", symbol.fqn.as_deref().unwrap_or("?"));
475    /// }
476    /// # Ok::<(), anyhow::Error>(())
477    /// ```
478    pub fn reachable_symbols(&self, symbol_id: &str) -> Result<Vec<SymbolInfo>> {
479        self.graph.reachable_symbols(symbol_id, None)
480    }
481
482    /// Find all symbols that can reach a given symbol (reverse reachability)
483    ///
484    /// Computes the reverse transitive closure of the call graph. Returns all
485    /// symbols from which the specified symbol can be reached (i.e., all callers).
486    /// This is useful for:
487    /// - Bug isolation (what code affects this symbol?)
488    /// - Refactoring safety (what needs to be updated?)
489    /// - Root cause analysis
490    ///
491    /// # Arguments
492    ///
493    /// * `symbol_id` - Stable symbol ID (32-char BLAKE3 hash) or FQN
494    ///
495    /// # Returns
496    ///
497    /// Vector of [`SymbolInfo`] for symbols that can reach the target
498    ///
499    /// # Example
500    ///
501    /// ```no_run
502    /// use mirage_analyzer::analysis::MagellanBridge;
503    ///
504    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
505    ///
506    /// // Find all functions that call 'helper_function'
507    /// let callers = bridge.reverse_reachable_symbols("helper_function")?;
508    /// println!("{} functions call this", callers.len());
509    /// # Ok::<(), anyhow::Error>(())
510    /// ```
511    pub fn reverse_reachable_symbols(&self, symbol_id: &str) -> Result<Vec<SymbolInfo>> {
512        self.graph.reverse_reachable_symbols(symbol_id, None)
513    }
514
515    /// Find dead code unreachable from an entry point symbol
516    ///
517    /// Identifies all symbols in the call graph that cannot be reached from
518    /// the specified entry point (e.g., `main`, `test_main`).
519    ///
520    /// # Limitations
521    ///
522    /// - Only considers the call graph
523    /// - Symbols called via reflection, function pointers, or dynamic dispatch
524    ///   may be incorrectly flagged
525    /// - Test functions and platform-specific code may appear as dead code
526    ///
527    /// # Arguments
528    ///
529    /// * `entry_symbol_id` - Stable symbol ID of the entry point (e.g., main function)
530    ///
531    /// # Returns
532    ///
533    /// Vector of [`DeadSymbol`] for unreachable symbols
534    ///
535    /// # Example
536    ///
537    /// ```no_run
538    /// use mirage_analyzer::analysis::MagellanBridge;
539    /// use mirage_analyzer::cli::DeadSymbolJson;
540    ///
541    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
542    ///
543    /// // Find all functions unreachable from main
544    /// let dead = bridge.dead_symbols("main")?;
545    /// for dead_symbol in &dead {
546    ///     println!("Dead: {} ({})",
547    ///         dead_symbol.symbol.fqn.as_deref().unwrap_or("?"),
548    ///         dead_symbol.reason);
549    /// }
550    ///
551    /// // Convert to JSON-serializable format
552    /// let json_symbols: Vec<DeadSymbolJson> = dead.iter().map(|d| d.into()).collect();
553    /// # Ok::<(), anyhow::Error>(())
554    /// ```
555    pub fn dead_symbols(&self, entry_symbol_id: &str) -> Result<Vec<DeadSymbol>> {
556        self.graph.dead_symbols(entry_symbol_id)
557    }
558
559    /// Detect cycles in the call graph using SCC decomposition
560    ///
561    /// Finds all strongly connected components (SCCs) with more than one member,
562    /// which indicate cycles or mutual recursion in the call graph.
563    ///
564    /// # Returns
565    ///
566    /// [`CycleReport`] containing all detected cycles
567    ///
568    /// # Example
569    ///
570    /// ```no_run
571    /// use mirage_analyzer::analysis::MagellanBridge;
572    ///
573    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
574    ///
575    /// let report = bridge.detect_cycles()?;
576    /// println!("Found {} cycles", report.total_count);
577    /// for cycle in &report.cycles {
578    ///     println!("Cycle with {} members:", cycle.members.len());
579    ///     for member in &cycle.members {
580    ///         println!("  - {}", member.fqn.as_deref().unwrap_or("?"));
581    ///     }
582    /// }
583    /// # Ok::<(), anyhow::Error>(())
584    /// ```
585    pub fn detect_cycles(&self) -> Result<CycleReport> {
586        self.graph.detect_cycles()
587    }
588
589    /// Compute a backward program slice (what affects this symbol)
590    ///
591    /// Returns all symbols that can affect the target symbol through the call graph.
592    /// This is useful for bug isolation.
593    ///
594    /// # Note
595    ///
596    /// Current implementation uses call-graph reachability as a fallback.
597    /// Full CFG-based program slicing will be available in future versions.
598    ///
599    /// # Arguments
600    ///
601    /// * `symbol_id` - Stable symbol ID or FQN to slice from
602    ///
603    /// # Returns
604    ///
605    /// [`SliceResult`] containing the slice and statistics
606    ///
607    /// # Example
608    ///
609    /// ```no_run
610    /// use mirage_analyzer::analysis::MagellanBridge;
611    ///
612    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
613    ///
614    /// // Find what affects 'helper_function'
615    /// let slice_result = bridge.backward_slice("helper_function")?;
616    /// println!("{} symbols affect this function", slice_result.symbol_count);
617    /// # Ok::<(), anyhow::Error>(())
618    /// ```
619    pub fn backward_slice(&self, symbol_id: &str) -> Result<SliceWrapper> {
620        let result = self.graph.backward_slice(symbol_id)?;
621        Ok((&result).into())
622    }
623
624    /// Compute a forward program slice (what this symbol affects)
625    ///
626    /// Returns all symbols that the target symbol can affect through the call graph.
627    /// This is useful for refactoring safety.
628    ///
629    /// # Note
630    ///
631    /// Current implementation uses call-graph reachability as a fallback.
632    /// Full CFG-based program slicing will be available in future versions.
633    ///
634    /// # Arguments
635    ///
636    /// * `symbol_id` - Stable symbol ID or FQN to slice from
637    ///
638    /// # Returns
639    ///
640    /// [`SliceWrapper`] containing the slice and statistics
641    ///
642    /// # Example
643    ///
644    /// ```no_run
645    /// use mirage_analyzer::analysis::MagellanBridge;
646    ///
647    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
648    ///
649    /// // Find what 'main_function' affects
650    /// let slice_result = bridge.forward_slice("main_function")?;
651    /// println!("{} symbols are affected by this function", slice_result.symbol_count);
652    /// # Ok::<(), anyhow::Error>(())
653    /// ```
654    pub fn forward_slice(&self, symbol_id: &str) -> Result<SliceWrapper> {
655        let result = self.graph.forward_slice(symbol_id)?;
656        Ok((&result).into())
657    }
658
659    /// Enumerate execution paths from a starting symbol
660    ///
661    /// Finds all execution paths from `start_symbol_id` to `end_symbol_id` (if provided)
662    /// or all paths starting from `start_symbol_id` (if end_symbol_id is None).
663    ///
664    /// Path enumeration uses bounded DFS to prevent infinite traversal in cyclic graphs.
665    ///
666    /// # Arguments
667    ///
668    /// * `start_symbol_id` - Starting symbol ID or FQN
669    /// * `end_symbol_id` - Optional ending symbol ID or FQN
670    /// * `max_depth` - Maximum path depth (default: 100)
671    /// * `max_paths` - Maximum number of paths to return (default: 1000)
672    ///
673    /// # Returns
674    ///
675    /// [`PathEnumerationResult`] with all discovered paths and statistics
676    ///
677    /// # Example
678    ///
679    /// ```no_run
680    /// use mirage_analyzer::analysis::MagellanBridge;
681    ///
682    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
683    ///
684    /// // Find all paths from main to any leaf function
685    /// let result = bridge.enumerate_paths("main", None, 50, 100)?;
686    ///
687    /// println!("Found {} paths", result.total_enumerated);
688    /// println!("Average length: {:.2}", result.statistics.avg_length);
689    /// # Ok::<(), anyhow::Error>(())
690    /// ```
691    pub fn enumerate_paths(
692        &self,
693        start_symbol_id: &str,
694        end_symbol_id: Option<&str>,
695        max_depth: usize,
696        max_paths: usize,
697    ) -> Result<PathEnumerationResult> {
698        self.graph
699            .enumerate_paths(start_symbol_id, end_symbol_id, max_depth, max_paths)
700    }
701
702    /// Enumerate paths and return JSON-serializable result
703    ///
704    /// Convenience method that wraps [`PathEnumerationResult`] in a
705    /// JSON-serializable format for CLI output.
706    ///
707    /// # Arguments
708    ///
709    /// * `start_symbol_id` - Starting symbol ID or FQN
710    /// * `end_symbol_id` - Optional ending symbol ID or FQN
711    /// * `max_depth` - Maximum path depth (default: 100)
712    /// * `max_paths` - Maximum number of paths to return (default: 1000)
713    ///
714    /// # Returns
715    ///
716    /// JSON-serializable path enumeration result
717    ///
718    /// # Example
719    ///
720    /// ```no_run
721    /// use mirage_analyzer::analysis::MagellanBridge;
722    ///
723    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
724    /// let result = bridge.enumerate_paths_json("main", None, 50, 100)?;
725    /// println!("Found {} paths", result.total_enumerated);
726    /// # Ok::<(), anyhow::Error>(())
727    /// ```
728    pub fn enumerate_paths_json(
729        &self,
730        start_symbol_id: &str,
731        end_symbol_id: Option<&str>,
732        max_depth: usize,
733        max_paths: usize,
734    ) -> Result<PathEnumerationJson> {
735        let result =
736            self.graph
737                .enumerate_paths(start_symbol_id, end_symbol_id, max_depth, max_paths)?;
738        Ok((&result).into())
739    }
740
741    /// Condense the call graph by collapsing SCCs into supernodes
742    ///
743    /// Creates a condensation DAG by collapsing each strongly connected component
744    /// into a single "supernode". The resulting graph is always acyclic.
745    ///
746    /// # Use Cases
747    ///
748    /// - **Topological Sorting**: Condensation graph is a DAG
749    /// - **Mutual Recursion Detection**: Large supernodes indicate tight coupling
750    /// - **Impact Analysis**: Changing one symbol affects its entire SCC
751    /// - **Inter-procedural Dominance**: Functions in root supernodes dominate downstream functions
752    ///
753    /// # Returns
754    ///
755    /// [`CondensationResult`] with the condensed DAG and symbol-to-supernode mapping
756    ///
757    /// # Example
758    ///
759    /// ```no_run
760    /// use mirage_analyzer::analysis::MagellanBridge;
761    ///
762    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
763    ///
764    /// let condensed = bridge.condense_call_graph()?;
765    ///
766    /// println!("Condensed to {} supernodes", condensed.graph.supernodes.len());
767    /// println!("Condensed graph has {} edges", condensed.graph.edges.len());
768    /// # Ok::<(), anyhow::Error>(())
769    /// ```
770    pub fn condense_call_graph(&self) -> Result<CondensationResult> {
771        self.graph.condense_call_graph()
772    }
773
774    /// Condense call graph and return JSON-serializable result
775    ///
776    /// Convenience method that wraps [`CondensationResult`] in a
777    /// JSON-serializable format for CLI output.
778    ///
779    /// # Returns
780    ///
781    /// [`CondensationJson`] with condensed DAG summary and supernode details
782    ///
783    /// # Example
784    ///
785    /// ```no_run
786    /// use mirage_analyzer::analysis::MagellanBridge;
787    ///
788    /// let bridge = MagellanBridge::open("codemcp/mirage.db")?;
789    /// let condensed = bridge.condense_call_graph_json()?;
790    /// println!("Condensed to {} supernodes", condensed.supernode_count);
791    /// println!("Largest SCC has {} functions", condensed.largest_scc_size);
792    /// # Ok::<(), anyhow::Error>(())
793    /// ```
794    pub fn condense_call_graph_json(&self) -> Result<CondensationJson> {
795        let result = self.graph.condense_call_graph()?;
796        Ok((&result).into())
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[test]
805    fn test_magellan_bridge_creation() {
806        // Test that MagellanBridge can be created (requires database)
807        // This is a compile-time test - actual database integration tested in later plans
808        let _ = || -> Result<()> {
809            let _bridge = MagellanBridge::open("test.db")?;
810            Ok(())
811        };
812    }
813
814    #[test]
815    fn test_dead_symbol_json_from_dead_symbol() {
816        // Test DeadSymbolJson conversion from DeadSymbol
817        use magellan::{DeadSymbol as MagellanDeadSymbol, SymbolInfo};
818
819        let symbol_info = SymbolInfo {
820            symbol_id: Some("test_symbol_id".to_string()),
821            fqn: Some("test::function".to_string()),
822            file_path: "test.rs".to_string(),
823            kind: "Function".to_string(),
824        };
825
826        let dead = MagellanDeadSymbol {
827            symbol: symbol_info,
828            reason: "Not called from entry point".to_string(),
829        };
830
831        let json_symbol: DeadSymbolJson = (&dead).into();
832
833        assert_eq!(json_symbol.fqn, Some("test::function".to_string()));
834        assert_eq!(json_symbol.file_path, "test.rs");
835        assert_eq!(json_symbol.kind, "Function");
836        assert_eq!(json_symbol.reason, "Not called from entry point");
837    }
838
839    #[test]
840    fn test_enhanced_dead_code_serialization() {
841        // Test EnhancedDeadCode can be serialized to JSON
842        use magellan::{DeadSymbol as MagellanDeadSymbol, SymbolInfo};
843
844        let symbol_info = SymbolInfo {
845            symbol_id: Some("test_id".to_string()),
846            fqn: Some("dead::function".to_string()),
847            file_path: "test.rs".to_string(),
848            kind: "Function".to_string(),
849        };
850
851        let dead = MagellanDeadSymbol {
852            symbol: symbol_info,
853            reason: "Uncalled".to_string(),
854        };
855
856        let json_symbol: DeadSymbolJson = (&dead).into();
857
858        let mut unreachable_blocks = std::collections::HashMap::new();
859        unreachable_blocks.insert("test_func".to_string(), vec![1, 2, 3]);
860
861        let enhanced = EnhancedDeadCode {
862            uncalled_functions: vec![json_symbol],
863            unreachable_blocks,
864            total_dead_count: 4,
865        };
866
867        // Test serialization
868        let json = serde_json::to_string(&enhanced).unwrap();
869        assert!(json.contains("uncalled_functions"));
870        assert!(json.contains("unreachable_blocks"));
871        assert!(json.contains("total_dead_count"));
872    }
873
874    #[test]
875    fn test_cycle_info_from_cycle() {
876        // Test CycleInfo conversion from Cycle
877        use magellan::{Cycle, CycleKind, SymbolInfo};
878
879        let symbol1 = SymbolInfo {
880            symbol_id: Some("func_a_id".to_string()),
881            fqn: Some("func_a".to_string()),
882            file_path: "test.rs".to_string(),
883            kind: "Function".to_string(),
884        };
885
886        let symbol2 = SymbolInfo {
887            symbol_id: Some("func_b_id".to_string()),
888            fqn: Some("func_b".to_string()),
889            file_path: "test.rs".to_string(),
890            kind: "Function".to_string(),
891        };
892
893        // Test MutualRecursion cycle
894        let mutual_recursion_cycle = Cycle {
895            members: vec![symbol1.clone(), symbol2.clone()],
896            kind: CycleKind::MutualRecursion,
897        };
898
899        let cycle_info: CycleInfo = (&mutual_recursion_cycle).into();
900        assert_eq!(cycle_info.cycle_type, "MutualRecursion");
901        assert_eq!(cycle_info.size, 2);
902        assert_eq!(cycle_info.members, vec!["func_a", "func_b"]);
903
904        // Test SelfLoop cycle
905        let self_loop_cycle = Cycle {
906            members: vec![symbol1],
907            kind: CycleKind::SelfLoop,
908        };
909
910        let cycle_info: CycleInfo = (&self_loop_cycle).into();
911        assert_eq!(cycle_info.cycle_type, "SelfLoop");
912        assert_eq!(cycle_info.size, 1);
913        assert_eq!(cycle_info.members, vec!["func_a"]);
914    }
915
916    #[test]
917    fn test_enhanced_cycles_serialization() {
918        // Test EnhancedCycles can be serialized to JSON
919        use std::collections::HashMap;
920
921        let mut function_loops = HashMap::new();
922        function_loops.insert(
923            "test_func".to_string(),
924            vec![LoopInfo {
925                header: 1,
926                back_edge_from: 2,
927                body_size: 3,
928                nesting_level: 0,
929                body_blocks: vec![1, 2, 3],
930            }],
931        );
932
933        let call_graph_cycles = vec![CycleInfo {
934            members: vec!["func_a".to_string(), "func_b".to_string()],
935            cycle_type: "MutualRecursion".to_string(),
936            size: 2,
937        }];
938
939        let enhanced = EnhancedCycles {
940            call_graph_cycles,
941            function_loops,
942            total_cycles: 2,
943        };
944
945        // Test serialization
946        let json = serde_json::to_string(&enhanced).unwrap();
947        assert!(json.contains("call_graph_cycles"));
948        assert!(json.contains("function_loops"));
949        assert!(json.contains("total_cycles"));
950        assert!(json.contains("MutualRecursion"));
951    }
952
953    #[test]
954    fn test_loop_info_serialization() {
955        // Test LoopInfo can be serialized to JSON
956        let loop_info = LoopInfo {
957            header: 1,
958            back_edge_from: 3,
959            body_size: 5,
960            nesting_level: 2,
961            body_blocks: vec![1, 2, 3, 4, 5],
962        };
963
964        // Test serialization
965        let json = serde_json::to_string(&loop_info).unwrap();
966        assert!(json.contains(r#""header":1"#));
967        assert!(json.contains(r#""back_edge_from":3"#));
968        assert!(json.contains(r#""body_size":5"#));
969        assert!(json.contains(r#""nesting_level":2"#));
970        assert!(json.contains(r#"body_blocks"#));
971    }
972
973    #[test]
974    fn test_slice_wrapper_serialization() {
975        // Test SliceWrapper can be serialized to JSON
976        use magellan::{ProgramSlice, SliceDirection, SliceResult, SliceStatistics};
977
978        let target = SymbolInfo {
979            symbol_id: Some("target_id".to_string()),
980            fqn: Some("target_function".to_string()),
981            file_path: "test.rs".to_string(),
982            kind: "Function".to_string(),
983        };
984
985        let included_symbols = vec![
986            SymbolInfo {
987                symbol_id: Some("sym1_id".to_string()),
988                fqn: Some("sym1".to_string()),
989                file_path: "test.rs".to_string(),
990                kind: "Function".to_string(),
991            },
992            SymbolInfo {
993                symbol_id: Some("sym2_id".to_string()),
994                fqn: Some("sym2".to_string()),
995                file_path: "test.rs".to_string(),
996                kind: "Function".to_string(),
997            },
998        ];
999
1000        let program_slice = ProgramSlice {
1001            target: target.clone(),
1002            direction: SliceDirection::Backward,
1003            included_symbols: included_symbols.clone(),
1004            symbol_count: 3,
1005        };
1006
1007        let statistics = SliceStatistics {
1008            total_symbols: 3,
1009            data_dependencies: 2,
1010            control_dependencies: 1,
1011        };
1012
1013        let slice_result = SliceResult {
1014            slice: program_slice,
1015            statistics,
1016        };
1017
1018        let wrapper: SliceWrapper = (&slice_result).into();
1019
1020        // Test wrapper fields
1021        assert_eq!(wrapper.target.fqn, Some("target_function".to_string()));
1022        assert_eq!(wrapper.direction, "Backward");
1023        assert_eq!(wrapper.symbol_count, 3);
1024        assert_eq!(wrapper.statistics.total_symbols, 3);
1025        assert_eq!(wrapper.statistics.data_dependencies, 2);
1026        assert_eq!(wrapper.statistics.control_dependencies, 1);
1027        assert_eq!(wrapper.included_symbols.len(), 2);
1028
1029        // Test serialization
1030        let json = serde_json::to_string(&wrapper).unwrap();
1031        assert!(json.contains("target"));
1032        assert!(json.contains("direction"));
1033        assert!(json.contains("Backward"));
1034        assert!(json.contains("included_symbols"));
1035        assert!(json.contains("statistics"));
1036        assert!(json.contains("data_dependencies"));
1037    }
1038
1039    #[test]
1040    fn test_slice_stats_creation() {
1041        // Test SliceStats struct creation
1042        let stats = SliceStats {
1043            total_symbols: 10,
1044            data_dependencies: 5,
1045            control_dependencies: 3,
1046        };
1047
1048        assert_eq!(stats.total_symbols, 10);
1049        assert_eq!(stats.data_dependencies, 5);
1050        assert_eq!(stats.control_dependencies, 3);
1051
1052        // Test serialization
1053        let json = serde_json::to_string(&stats).unwrap();
1054        assert!(json.contains(r#""total_symbols":10"#));
1055        assert!(json.contains(r#""data_dependencies":5"#));
1056        assert!(json.contains(r#""control_dependencies":3"#));
1057    }
1058
1059    #[test]
1060    fn test_symbol_info_json_from_symbol_info() {
1061        // Test SymbolInfoJson conversion from SymbolInfo
1062        use magellan::SymbolInfo;
1063
1064        let symbol_info = SymbolInfo {
1065            symbol_id: Some("test_symbol_id".to_string()),
1066            fqn: Some("test::function".to_string()),
1067            file_path: "test.rs".to_string(),
1068            kind: "Function".to_string(),
1069        };
1070
1071        let json_symbol: SymbolInfoJson = (&symbol_info).into();
1072
1073        assert_eq!(json_symbol.symbol_id, Some("test_symbol_id".to_string()));
1074        assert_eq!(json_symbol.fqn, Some("test::function".to_string()));
1075        assert_eq!(json_symbol.file_path, "test.rs");
1076        assert_eq!(json_symbol.kind, "Function");
1077    }
1078
1079    #[test]
1080    fn test_enhanced_blast_zone_creation() {
1081        // Test EnhancedBlastZone struct creation and serialization
1082        let forward = vec![
1083            SymbolInfoJson {
1084                symbol_id: Some("func_a_id".to_string()),
1085                fqn: Some("func_a".to_string()),
1086                file_path: "a.rs".to_string(),
1087                kind: "Function".to_string(),
1088            },
1089            SymbolInfoJson {
1090                symbol_id: Some("func_b_id".to_string()),
1091                fqn: Some("func_b".to_string()),
1092                file_path: "b.rs".to_string(),
1093                kind: "Function".to_string(),
1094            },
1095        ];
1096
1097        let backward = vec![SymbolInfoJson {
1098            symbol_id: Some("main_id".to_string()),
1099            fqn: Some("main".to_string()),
1100            file_path: "main.rs".to_string(),
1101            kind: "Function".to_string(),
1102        }];
1103
1104        let path_impact = PathImpactSummary {
1105            path_id: Some("test_path_id".to_string()),
1106            path_length: 5,
1107            blocks_affected: vec![1, 2, 3, 4],
1108            unique_blocks_count: 4,
1109        };
1110
1111        let blast_zone = EnhancedBlastZone {
1112            target: "test_function".to_string(),
1113            forward_reachable: forward.clone(),
1114            backward_reachable: backward.clone(),
1115            path_impact: Some(path_impact),
1116        };
1117
1118        assert_eq!(blast_zone.target, "test_function");
1119        assert_eq!(blast_zone.forward_reachable.len(), 2);
1120        assert_eq!(blast_zone.backward_reachable.len(), 1);
1121        assert!(blast_zone.path_impact.is_some());
1122
1123        // Test serialization
1124        let json = serde_json::to_string(&blast_zone).unwrap();
1125        assert!(json.contains("target"));
1126        assert!(json.contains("forward_reachable"));
1127        assert!(json.contains("backward_reachable"));
1128        assert!(json.contains("path_impact"));
1129        assert!(json.contains("func_a"));
1130        assert!(json.contains("main"));
1131    }
1132
1133    #[test]
1134    fn test_path_impact_summary_serialization() {
1135        // Test PathImpactSummary can be serialized to JSON
1136        let impact = PathImpactSummary {
1137            path_id: Some("test_path".to_string()),
1138            path_length: 10,
1139            blocks_affected: vec![1, 2, 3, 4, 5],
1140            unique_blocks_count: 5,
1141        };
1142
1143        let json = serde_json::to_string(&impact).unwrap();
1144        assert!(json.contains("path_id"));
1145        assert!(json.contains("path_length"));
1146        assert!(json.contains("blocks_affected"));
1147        assert!(json.contains("unique_blocks_count"));
1148        assert!(json.contains("test_path"));
1149    }
1150
1151    #[test]
1152    fn test_enhanced_blast_zone_without_path_impact() {
1153        // Test EnhancedBlastZone without optional path_impact
1154        let blast_zone = EnhancedBlastZone {
1155            target: "test_function".to_string(),
1156            forward_reachable: vec![],
1157            backward_reachable: vec![],
1158            path_impact: None,
1159        };
1160
1161        assert!(blast_zone.path_impact.is_none());
1162
1163        // Test serialization with None
1164        let json = serde_json::to_string(&blast_zone).unwrap();
1165        assert!(json.contains(r#""path_impact":null"#));
1166    }
1167
1168    #[test]
1169    fn test_condensation_json_creation() {
1170        // Test CondensationJson struct creation and serialization
1171        use magellan::{CondensationGraph, CondensationResult, Supernode};
1172        use std::collections::HashMap;
1173
1174        // Create test supernodes
1175        let symbol1 = SymbolInfo {
1176            symbol_id: Some("func_a_id".to_string()),
1177            fqn: Some("func_a".to_string()),
1178            file_path: "a.rs".to_string(),
1179            kind: "Function".to_string(),
1180        };
1181
1182        let symbol2 = SymbolInfo {
1183            symbol_id: Some("func_b_id".to_string()),
1184            fqn: Some("func_b".to_string()),
1185            file_path: "b.rs".to_string(),
1186            kind: "Function".to_string(),
1187        };
1188
1189        let supernode1 = Supernode {
1190            id: 0,
1191            members: vec![symbol1.clone()],
1192        };
1193
1194        let supernode2 = Supernode {
1195            id: 1,
1196            members: vec![symbol2.clone(), symbol1.clone()], // SCC with 2 functions
1197        };
1198
1199        let graph = CondensationGraph {
1200            supernodes: vec![supernode1, supernode2],
1201            edges: vec![(0, 1)],
1202        };
1203
1204        let mut mapping = HashMap::new();
1205        mapping.insert("func_a".to_string(), 0);
1206        mapping.insert("func_b".to_string(), 1);
1207
1208        let result = CondensationResult {
1209            graph,
1210            original_to_supernode: mapping,
1211        };
1212
1213        let json: CondensationJson = (&result).into();
1214
1215        assert_eq!(json.supernode_count, 2);
1216        assert_eq!(json.edge_count, 1);
1217        assert_eq!(json.largest_scc_size, 2);
1218        assert_eq!(json.supernodes.len(), 2);
1219        assert_eq!(json.supernodes[0].id, "0");
1220        assert_eq!(json.supernodes[0].member_count, 1);
1221        assert_eq!(json.supernodes[1].id, "1");
1222        assert_eq!(json.supernodes[1].member_count, 2);
1223        assert!(json.supernodes[1].members.contains(&"func_b".to_string()));
1224    }
1225
1226    #[test]
1227    fn test_condensation_json_serialization() {
1228        // Test CondensationJson can be serialized to JSON
1229        use magellan::{CondensationGraph, CondensationResult, Supernode};
1230        use std::collections::HashMap;
1231
1232        let supernode = Supernode {
1233            id: 0,
1234            members: vec![SymbolInfo {
1235                symbol_id: Some("test_id".to_string()),
1236                fqn: Some("test_func".to_string()),
1237                file_path: "test.rs".to_string(),
1238                kind: "Function".to_string(),
1239            }],
1240        };
1241
1242        let graph = CondensationGraph {
1243            supernodes: vec![supernode],
1244            edges: vec![],
1245        };
1246
1247        let result = CondensationResult {
1248            graph,
1249            original_to_supernode: HashMap::new(),
1250        };
1251
1252        let json: CondensationJson = (&result).into();
1253        let json_string = serde_json::to_string(&json).unwrap();
1254
1255        assert!(json_string.contains(r#""supernode_count":1"#));
1256        assert!(json_string.contains(r#""edge_count":0"#));
1257        assert!(json_string.contains(r#""largest_scc_size":1"#));
1258        assert!(json_string.contains(r#""id":"0""#));
1259        assert!(json_string.contains("test_func"));
1260    }
1261
1262    #[test]
1263    fn test_supernode_json_creation() {
1264        // Test SupernodeJson struct creation
1265        let supernode = SupernodeJson {
1266            id: "42".to_string(),
1267            member_count: 3,
1268            members: vec![
1269                "func_a".to_string(),
1270                "func_b".to_string(),
1271                "func_c".to_string(),
1272            ],
1273        };
1274
1275        assert_eq!(supernode.id, "42");
1276        assert_eq!(supernode.member_count, 3);
1277        assert_eq!(supernode.members.len(), 3);
1278
1279        let json = serde_json::to_string(&supernode).unwrap();
1280        assert!(json.contains(r#""id":"42""#));
1281        assert!(json.contains(r#""member_count":3"#));
1282        assert!(json.contains("func_a"));
1283    }
1284
1285    #[test]
1286    fn test_execution_path_json_conversion() {
1287        use magellan::{ExecutionPath, SymbolInfo};
1288
1289        let symbols = vec![
1290            SymbolInfo {
1291                symbol_id: Some("main_id".to_string()),
1292                fqn: Some("main".to_string()),
1293                file_path: "main.rs".to_string(),
1294                kind: "Function".to_string(),
1295            },
1296            SymbolInfo {
1297                symbol_id: Some("helper_id".to_string()),
1298                fqn: Some("helper".to_string()),
1299                file_path: "helper.rs".to_string(),
1300                kind: "Function".to_string(),
1301            },
1302        ];
1303
1304        let path = ExecutionPath {
1305            symbols: symbols.clone(),
1306            length: 2,
1307        };
1308
1309        let json_path: ExecutionPathJson = (&path).into();
1310
1311        assert_eq!(json_path.length, 2);
1312        assert_eq!(json_path.symbols.len(), 2);
1313        assert_eq!(json_path.symbols[0].fqn, Some("main".to_string()));
1314        assert_eq!(json_path.symbols[1].fqn, Some("helper".to_string()));
1315
1316        // Test serialization
1317        let json = serde_json::to_string(&json_path).unwrap();
1318        assert!(json.contains("symbols"));
1319        assert!(json.contains("length"));
1320        assert!(json.contains("main"));
1321        assert!(json.contains("helper"));
1322    }
1323
1324    #[test]
1325    fn test_path_statistics_json_creation() {
1326        let stats = PathStatisticsJson {
1327            avg_length: 3.5,
1328            max_length: 10,
1329            min_length: 1,
1330            unique_symbols: 5,
1331        };
1332
1333        assert_eq!(stats.avg_length, 3.5);
1334        assert_eq!(stats.max_length, 10);
1335        assert_eq!(stats.min_length, 1);
1336        assert_eq!(stats.unique_symbols, 5);
1337
1338        // Test serialization
1339        let json = serde_json::to_string(&stats).unwrap();
1340        assert!(json.contains(r#""avg_length":3.5"#));
1341        assert!(json.contains(r#""max_length":10"#));
1342        assert!(json.contains(r#""min_length":1"#));
1343        assert!(json.contains(r#""unique_symbols":5"#));
1344    }
1345
1346    #[test]
1347    fn test_path_enumeration_json_serialization() {
1348        use magellan::{ExecutionPath, PathStatistics};
1349
1350        let symbols = vec![SymbolInfo {
1351            symbol_id: Some("func1_id".to_string()),
1352            fqn: Some("func1".to_string()),
1353            file_path: "test.rs".to_string(),
1354            kind: "Function".to_string(),
1355        }];
1356
1357        let _path = ExecutionPath { symbols, length: 1 };
1358
1359        let _stats = PathStatistics {
1360            avg_length: 2.0,
1361            max_length: 5,
1362            min_length: 1,
1363            unique_symbols: 3,
1364        };
1365
1366        // We can't directly construct PathEnumerationResult as its fields are private
1367        // This test verifies the JSON structure would serialize correctly
1368        let json_stats = PathStatisticsJson {
1369            avg_length: 2.0,
1370            max_length: 5,
1371            min_length: 1,
1372            unique_symbols: 3,
1373        };
1374
1375        let json = serde_json::to_string(&json_stats).unwrap();
1376        assert!(json.contains("avg_length"));
1377        assert!(json.contains("max_length"));
1378        assert!(json.contains("min_length"));
1379        assert!(json.contains("unique_symbols"));
1380    }
1381
1382    #[test]
1383    fn test_execution_path_json_empty_path() {
1384        use magellan::ExecutionPath;
1385
1386        let path = ExecutionPath {
1387            symbols: vec![],
1388            length: 0,
1389        };
1390
1391        let json_path: ExecutionPathJson = (&path).into();
1392
1393        assert_eq!(json_path.length, 0);
1394        assert_eq!(json_path.symbols.len(), 0);
1395
1396        // Test serialization with empty arrays
1397        let json = serde_json::to_string(&json_path).unwrap();
1398        assert!(json.contains(r#""symbols":[]"#));
1399        assert!(json.contains(r#""length":0"#));
1400    }
1401
1402    // ============================================================================
1403    // Phase 11 Comprehensive Tests
1404    // ============================================================================
1405
1406    /// Test condensation JSON from result (SC 8: Inter-procedural Dominance)
1407    #[test]
1408    fn test_condensation_json_from_result() {
1409        // Test CondensationJson creation
1410        let json = CondensationJson {
1411            supernode_count: 5,
1412            edge_count: 8,
1413            supernodes: vec![SupernodeJson {
1414                id: "scc0".to_string(),
1415                member_count: 3,
1416                members: vec![
1417                    "func_a".to_string(),
1418                    "func_b".to_string(),
1419                    "func_c".to_string(),
1420                ],
1421            }],
1422            largest_scc_size: 3,
1423        };
1424
1425        assert_eq!(json.supernode_count, 5);
1426        assert_eq!(json.largest_scc_size, 3);
1427        assert_eq!(json.supernodes[0].member_count, 3);
1428
1429        // Test serialization
1430        let serialized = serde_json::to_string(&json).unwrap();
1431        assert!(serialized.contains("supernode_count"));
1432        assert!(serialized.contains("largest_scc_size"));
1433    }
1434
1435    /// Test execution path JSON serialization (SC 9: Path-based Hotspot Analysis)
1436    #[test]
1437    fn test_execution_path_json_serialization() {
1438        let path = ExecutionPathJson {
1439            symbols: vec![SymbolInfoJson {
1440                symbol_id: Some("id1".to_string()),
1441                fqn: Some("main".to_string()),
1442                file_path: "main.rs".to_string(),
1443                kind: "Function".to_string(),
1444            }],
1445            length: 1,
1446        };
1447
1448        let json = serde_json::to_string(&path).unwrap();
1449        assert!(json.contains("main"));
1450        assert!(json.contains("\"length\":1"));
1451    }
1452
1453    /// Test all Magellan imports are utilized (compile-time verification)
1454    ///
1455    /// This test verifies that all Magellan imports are accessible.
1456    /// If any import is truly unused at the module level, rustc would warn about it.
1457    #[test]
1458    fn test_all_magellan_imports_utilized() {
1459        // Verify CondensationGraph types are accessible (used in tests)
1460        let _ = std::marker::PhantomData::<CondensationGraph>;
1461        let _ = std::marker::PhantomData::<CondensationResult>;
1462        let _ = std::marker::PhantomData::<Supernode>;
1463
1464        // Verify path enumeration types are accessible (used in tests)
1465        let _ = std::marker::PhantomData::<ExecutionPath>;
1466        let _ = std::marker::PhantomData::<PathEnumerationResult>;
1467        let _ = std::marker::PhantomData::<PathStatistics>;
1468
1469        // Verify JSON wrappers are accessible and usable
1470        let _ = std::marker::PhantomData::<CondensationJson>;
1471        let _ = std::marker::PhantomData::<SupernodeJson>;
1472        let _ = std::marker::PhantomData::<ExecutionPathJson>;
1473        let _ = std::marker::PhantomData::<PathEnumerationJson>;
1474        let _ = std::marker::PhantomData::<PathStatisticsJson>;
1475
1476        // Verify program slicing types are accessible (used in tests)
1477        let _ = std::marker::PhantomData::<ProgramSlice>;
1478        let _ = std::marker::PhantomData::<SliceDirection>;
1479        let _ = std::marker::PhantomData::<SliceResult>;
1480        let _ = std::marker::PhantomData::<SliceStatistics>;
1481    }
1482
1483    /// Test Phase 11 integration: condensation + path enumeration
1484    #[test]
1485    fn test_phase_11_integration() {
1486        // Test that CondensationJson and PathEnumerationJson work together
1487        let condensation = CondensationJson {
1488            supernode_count: 2,
1489            edge_count: 1,
1490            supernodes: vec![SupernodeJson {
1491                id: "scc0".to_string(),
1492                member_count: 2,
1493                members: vec!["func_a".to_string(), "func_b".to_string()],
1494            }],
1495            largest_scc_size: 2,
1496        };
1497
1498        let path_stats = PathStatisticsJson {
1499            avg_length: 3.5,
1500            max_length: 10,
1501            min_length: 1,
1502            unique_symbols: 5,
1503        };
1504
1505        // Both should serialize independently
1506        let cond_json = serde_json::to_string(&condensation).unwrap();
1507        let stats_json = serde_json::to_string(&path_stats).unwrap();
1508
1509        assert!(cond_json.contains("supernode_count"));
1510        assert!(stats_json.contains("avg_length"));
1511    }
1512
1513    /// Test SupernodeJson with multiple members (SCC detection)
1514    #[test]
1515    fn test_supernode_json_multiple_members() {
1516        let supernode = SupernodeJson {
1517            id: "cycle_42".to_string(),
1518            member_count: 4,
1519            members: vec![
1520                "func_a".to_string(),
1521                "func_b".to_string(),
1522                "func_c".to_string(),
1523                "func_d".to_string(),
1524            ],
1525        };
1526
1527        let json = serde_json::to_string(&supernode).unwrap();
1528        assert!(json.contains("\"member_count\":4"));
1529        assert!(json.contains("cycle_42"));
1530        assert!(json.contains("func_a"));
1531        assert!(json.contains("func_d"));
1532    }
1533
1534    /// Test PathStatisticsJson with edge cases
1535    #[test]
1536    fn test_path_statistics_json_edge_cases() {
1537        // Empty paths
1538        let empty_stats = PathStatisticsJson {
1539            avg_length: 0.0,
1540            max_length: 0,
1541            min_length: 0,
1542            unique_symbols: 0,
1543        };
1544
1545        let json = serde_json::to_string(&empty_stats).unwrap();
1546        assert!(json.contains("\"avg_length\":0"));
1547        assert!(json.contains("\"unique_symbols\":0"));
1548
1549        // Large values
1550        let large_stats = PathStatisticsJson {
1551            avg_length: 9999.99,
1552            max_length: 100000,
1553            min_length: 1,
1554            unique_symbols: 50000,
1555        };
1556
1557        let json = serde_json::to_string(&large_stats).unwrap();
1558        assert!(json.contains("9999.99"));
1559        assert!(json.contains("100000"));
1560    }
1561}