Skip to main content

mirage/cli/
mod.rs

1// CLI command definitions following Magellan's CLI patterns
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5// Re-export for CLI use
6pub use crate::analysis::DeadSymbolJson;
7
8/// Mirage - Path-Aware Code Intelligence Engine
9///
10/// A control-flow and logic graph engine for Rust codebases.
11/// Extracts MIR from rustc, builds CFGs, enumerates execution paths.
12#[derive(Parser, Debug, Clone)]
13#[command(name = "mirage")]
14#[command(author, version, about)]
15#[command(
16    long_about = "Mirage is a path-aware code intelligence engine that operates on graphs, not text.
17
18It materializes behavior explicitly: paths, proofs, counterexamples.
19
20NOT:
21  - A search tool (llmgrep already does this)
22  - An embedding tool
23  - Static analysis / linting
24
25IS:
26  - Path enumeration and verification
27  - Graph-based reasoning about code behavior
28  - Truth engine that materializes facts for LLM consumption
29
30The Golden Rule: An agent may only speak if it can reference a graph artifact."
31)]
32pub struct Cli {
33    /// Path to the Magellan/Mirage database
34    #[arg(global = true, long, env = "MIRAGE_DB")]
35    pub db: Option<String>,
36
37    /// Output format
38    #[arg(global = true, long, value_enum, default_value_t = OutputFormat::Human)]
39    pub output: OutputFormat,
40
41    /// Detect and report backend format (sqlite or geometric)
42    #[arg(long, global = true, default_value = "false")]
43    pub detect_backend: bool,
44
45    #[command(subcommand)]
46    pub command: Option<Commands>,
47}
48
49/// Output format options
50#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OutputFormat {
52    /// Human-readable text output
53    Human,
54    /// Compact JSON for programmatic consumption
55    Json,
56    /// Formatted JSON with indentation
57    Pretty,
58}
59
60#[derive(Subcommand, Debug, Clone)]
61pub enum Commands {
62    /// Show database statistics
63    Status(StatusArgs),
64
65    /// Show all execution paths through a function
66    Paths(PathsArgs),
67
68    /// Show control-flow graph for a function
69    Cfg(CfgArgs),
70
71    /// Show dominance relationships for a function
72    Dominators(DominatorsArgs),
73
74    /// Show natural loops in CFG
75    Loops(LoopsArgs),
76
77    /// Find unreachable code within functions
78    Unreachable(UnreachableArgs),
79
80    /// Show branching patterns (if/else, match) in CFG
81    Patterns(PatternsArgs),
82
83    /// Show dominance frontiers in CFG
84    Frontiers(FrontiersArgs),
85
86    /// Verify a path is still valid
87    Verify(VerifyArgs),
88
89    /// Show impact analysis using paths (blast zone)
90    BlastZone(BlastZoneArgs),
91
92    /// Show cycles in code (call graph SCCs and function loops)
93    Cycles(CyclesArgs),
94
95    /// Perform program slicing (backward/forward impact analysis)
96    Slice(SliceArgs),
97
98    /// Show high-risk functions (hotspots)
99    Hotspots(HotspotsArgs),
100
101    /// Show most-traversed execution paths (hot paths)
102    Hotpaths(HotpathsArgs),
103
104    /// Show CFG differences between two snapshots
105    Diff(DiffArgs),
106
107    /// Show inter-procedural CFG (combined function CFGs with call/return edges)
108    Icfg(IcfgArgs),
109
110    /// Show per-block coverage for a function
111    Coverage(CoverageArgs),
112
113    /// Migrate database between storage backends
114    Migrate(MigrateArgs),
115}
116
117// ============================================================================
118// Query Commands
119// ============================================================================
120
121#[derive(Parser, Debug, Clone, Copy)]
122pub struct StatusArgs {}
123
124#[derive(Parser, Debug, Clone)]
125pub struct PathsArgs {
126    /// Function symbol ID or name
127    #[arg(long)]
128    pub function: String,
129
130    /// File path to disambiguate functions with same name (optional)
131    #[arg(long)]
132    pub file: Option<String>,
133
134    /// Show only error paths
135    #[arg(long)]
136    pub show_errors: bool,
137
138    /// Maximum path length (for pruning)
139    #[arg(long)]
140    pub max_length: Option<usize>,
141
142    /// Show block details for each path
143    #[arg(long)]
144    pub with_blocks: bool,
145
146    /// Incremental mode: analyze only changed functions since git revision
147    #[arg(long)]
148    pub incremental: bool,
149
150    /// Git revision for incremental analysis (e.g., "HEAD~1")
151    #[arg(long)]
152    pub since: Option<String>,
153
154    /// Sort paths by coverage hit count (highest first)
155    #[arg(long)]
156    pub by_coverage: bool,
157}
158
159#[derive(Parser, Debug, Clone)]
160pub struct CfgArgs {
161    /// Function symbol ID or name
162    #[arg(long)]
163    pub function: String,
164
165    /// File path to disambiguate functions with same name (optional)
166    #[arg(long)]
167    pub file: Option<String>,
168
169    /// Output format
170    #[arg(long, value_enum)]
171    pub format: Option<CfgFormat>,
172}
173
174#[derive(Parser, Debug, Clone)]
175pub struct CoverageArgs {
176    /// Function symbol ID or name
177    #[arg(long)]
178    pub function: String,
179
180    /// File path to disambiguate functions with same name (optional)
181    #[arg(long)]
182    pub file: Option<String>,
183}
184
185#[derive(Parser, Debug, Clone)]
186pub struct DominatorsArgs {
187    /// Function symbol ID or name
188    #[arg(long)]
189    pub function: String,
190
191    /// File path to disambiguate functions with same name (optional)
192    #[arg(long)]
193    pub file: Option<String>,
194
195    /// Show blocks that must pass through this block
196    #[arg(long)]
197    pub must_pass_through: Option<String>,
198
199    /// Show post-dominators instead of dominators
200    #[arg(long)]
201    pub post: bool,
202
203    /// Use inter-procedural (call graph) dominance instead of intra-procedural (CFG)
204    #[arg(long)]
205    pub inter_procedural: bool,
206}
207
208#[derive(Parser, Debug, Clone)]
209pub struct LoopsArgs {
210    /// Function to analyze for loops
211    #[arg(long)]
212    pub function: String,
213
214    /// File path to disambiguate functions with same name (optional)
215    #[arg(long)]
216    pub file: Option<String>,
217
218    /// Show detailed loop body blocks
219    #[arg(long)]
220    pub verbose: bool,
221}
222
223#[derive(Parser, Debug, Clone)]
224pub struct UnreachableArgs {
225    /// Find unreachable code within functions
226    #[arg(long)]
227    pub within_functions: bool,
228
229    /// Show branch details
230    #[arg(long)]
231    pub show_branches: bool,
232
233    /// Include uncalled functions (requires Magellan call graph)
234    #[arg(long)]
235    pub include_uncalled: bool,
236}
237
238#[derive(Parser, Debug, Clone)]
239pub struct PatternsArgs {
240    /// Function to analyze for branching patterns
241    #[arg(long)]
242    pub function: String,
243
244    /// File path to disambiguate functions with same name (optional)
245    #[arg(long)]
246    pub file: Option<String>,
247
248    /// Show only if/else patterns
249    #[arg(long)]
250    pub if_else: bool,
251
252    /// Show only match patterns
253    #[arg(long)]
254    pub r#match: bool,
255}
256
257#[derive(Parser, Debug, Clone)]
258pub struct FrontiersArgs {
259    /// Function to analyze for dominance frontiers
260    #[arg(long)]
261    pub function: String,
262
263    /// File path to disambiguate functions with same name (optional)
264    #[arg(long)]
265    pub file: Option<String>,
266
267    /// Show iterated dominance frontier (for phi placement)
268    #[arg(long)]
269    pub iterated: bool,
270
271    /// Show frontiers for specific node only
272    #[arg(long)]
273    pub node: Option<usize>,
274}
275
276#[derive(Parser, Debug, Clone)]
277pub struct VerifyArgs {
278    /// Path ID to verify
279    #[arg(long)]
280    pub path_id: String,
281}
282
283#[derive(Parser, Debug, Clone)]
284pub struct BlastZoneArgs {
285    /// Function symbol ID or name (for block-based analysis)
286    #[arg(long)]
287    pub function: Option<String>,
288
289    /// File path to disambiguate functions with same name (optional)
290    #[arg(long)]
291    pub file: Option<String>,
292
293    /// Block ID to analyze impact from (default: entry block 0)
294    #[arg(long)]
295    pub block_id: Option<usize>,
296
297    /// Path ID to analyze impact for
298    #[arg(long)]
299    pub path_id: Option<String>,
300
301    /// Maximum depth to traverse
302    #[arg(long, default_value_t = 100)]
303    pub max_depth: usize,
304
305    /// Include error paths in analysis
306    #[arg(long)]
307    pub include_errors: bool,
308
309    /// Use call graph for inter-procedural impact analysis
310    #[arg(long)]
311    pub use_call_graph: bool,
312}
313
314/// Cycle type filter for the cycles command
315#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
316pub enum CycleTypeArg {
317    /// Show all cycles (default)
318    All,
319    /// Show only inter-function cycles (mutual recursion, size > 1)
320    InterFunction,
321    /// Show only self-loops (single recursive function)
322    SelfLoop,
323}
324
325#[derive(Parser, Debug, Clone)]
326pub struct CyclesArgs {
327    /// Show call graph cycles (mutual recursion between functions)
328    #[arg(long)]
329    pub call_graph: bool,
330
331    /// Show function loops (within individual functions)
332    #[arg(long)]
333    pub function_loops: bool,
334
335    /// Show both types of cycles (default)
336    #[arg(long)]
337    pub both: bool,
338
339    /// Filter cycle type: all, inter-function, or self-loop
340    #[arg(long, value_enum, default_value = "all")]
341    pub cycle_type: CycleTypeArg,
342
343    /// Verbose output (show cycle members/loop bodies)
344    #[arg(long)]
345    pub verbose: bool,
346}
347
348#[derive(Parser, Debug, Clone)]
349pub struct SliceArgs {
350    /// Symbol ID or FQN to slice
351    #[arg(long)]
352    pub symbol: String,
353
354    /// Slice direction: backward (what affects) or forward (what affects)
355    #[arg(long, value_enum)]
356    pub direction: SliceDirectionArg,
357
358    /// Show detailed symbol information
359    #[arg(long)]
360    pub verbose: bool,
361}
362
363#[derive(Parser, Debug, Clone)]
364pub struct HotspotsArgs {
365    /// Entry point symbol (default: main)
366    #[arg(long, default_value = "main")]
367    pub entry: String,
368
369    /// Maximum number of hotspots to return
370    #[arg(long, default_value = "20")]
371    pub top: usize,
372
373    /// Minimum path count threshold
374    #[arg(long)]
375    pub min_paths: Option<usize>,
376
377    /// Show detailed metrics for each hotspot
378    #[arg(long)]
379    pub verbose: bool,
380
381    /// Use inter-procedural analysis (requires Magellan DB)
382    /// Enabled by default. Use --intra-procedural to force intra-procedural analysis.
383    #[arg(long, default_value = "true")]
384    pub inter_procedural: bool,
385
386    /// Use intra-procedural analysis only (faster, but may show 0 functions if cfg_blocks not populated)
387    #[arg(long, conflicts_with = "inter_procedural")]
388    pub intra_procedural: bool,
389}
390
391/// Hot path detection arguments
392#[derive(Parser, Debug, Clone)]
393pub struct HotpathsArgs {
394    /// Function symbol ID or name
395    #[arg(long)]
396    pub function: String,
397
398    /// Number of hot paths to return (default: 10)
399    #[arg(long, default_value = "10")]
400    pub top: usize,
401
402    /// Show rationale for hotness scores
403    #[arg(long)]
404    pub rationale: bool,
405
406    /// Minimum hotness threshold (0.0 to 1.0)
407    #[arg(long)]
408    pub min_score: Option<f64>,
409}
410
411/// Migrate database between storage backends
412#[derive(Parser, Debug, Clone)]
413pub struct MigrateArgs {
414    /// Source backend format
415    #[arg(long, value_enum)]
416    pub from: BackendFormat,
417
418    /// Target backend format
419    #[arg(long, value_enum)]
420    pub to: BackendFormat,
421
422    /// Database path to migrate
423    #[arg(short, long)]
424    pub db: String,
425
426    /// Create backup before migration
427    #[arg(long)]
428    pub backup: bool,
429
430    /// Dry run: detect format only without migrating
431    #[arg(long)]
432    pub dry_run: bool,
433}
434
435/// Inter-procedural CFG arguments
436#[derive(Parser, Debug, Clone)]
437pub struct IcfgArgs {
438    /// Entry function symbol ID or name
439    #[arg(long)]
440    pub entry: String,
441
442    /// Maximum depth for call graph traversal (default: 3)
443    #[arg(long, default_value = "3")]
444    pub depth: usize,
445
446    /// Include return edges (default: true)
447    #[arg(long, default_value = "true")]
448    pub return_edges: bool,
449
450    /// Output format
451    #[arg(long, value_enum)]
452    pub format: Option<IcfgFormat>,
453}
454
455/// ICFG output format
456#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
457pub enum IcfgFormat {
458    /// DOT graph format (for graphviz)
459    Dot,
460    /// JSON format
461    Json,
462    /// Human-readable summary
463    Human,
464}
465
466/// Diff command arguments
467#[derive(Parser, Debug, Clone)]
468pub struct DiffArgs {
469    /// Function symbol ID or name to compare
470    #[arg(long)]
471    pub function: String,
472
473    /// Before snapshot ID (transaction ID or "current")
474    #[arg(long)]
475    pub before: String,
476
477    /// After snapshot ID (transaction ID or "current")
478    #[arg(long)]
479    pub after: String,
480
481    /// Show edge differences
482    #[arg(long)]
483    pub show_edges: bool,
484
485    /// Show detailed block changes
486    #[arg(long)]
487    pub verbose: bool,
488}
489
490/// Backend format for migration
491#[derive(clap::ValueEnum, Clone, Debug, Copy, PartialEq, Eq)]
492pub enum BackendFormat {
493    /// SQLite database (traditional backend)
494    Sqlite,
495    /// Geometric database (.geo files, Magellan 3.0+)
496    Geometric,
497}
498
499impl std::fmt::Display for BackendFormat {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        match self {
502            Self::Sqlite => write!(f, "sqlite"),
503            Self::Geometric => write!(f, "geometric"),
504        }
505    }
506}
507
508#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
509pub enum SliceDirectionArg {
510    /// Backward: what affects this symbol
511    Backward,
512    /// Forward: what this symbol affects
513    Forward,
514}
515
516/// CFG output format
517#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
518pub enum CfgFormat {
519    /// Human-readable text
520    Human,
521    /// Graphviz DOT format
522    Dot,
523    /// JSON export
524    Json,
525}
526
527// ============================================================================
528// Utility Functions
529// ============================================================================
530
531/// Resolve the database path from multiple sources
532///
533/// Priority: CLI arg > MIRAGE_DB env var > auto-discover in common locations
534/// Auto-discovery searches: .magellan/*.db, .forge/*.db, *.db in current directory
535pub fn resolve_db_path(cli_db: Option<String>) -> anyhow::Result<String> {
536    if let Some(path) = cli_db {
537        return Ok(path);
538    }
539
540    // Try environment variable
541    if let Ok(path) = std::env::var("MIRAGE_DB") {
542        return Ok(path);
543    }
544
545    // Auto-discover database in common locations
546    if let Some(path) = auto_discover_db() {
547        eprintln!("Info: Auto-discovered database at {}", path);
548        return Ok(path);
549    }
550
551    Err(anyhow::anyhow!(
552        "No database specified. Use --db, set MIRAGE_DB env var, \
553         or run from a directory with a .db file"
554    ))
555}
556
557/// Auto-discover database file in common locations
558///
559/// Searches in priority order:
560/// 1. .magellan/*.db files (Magellan's conventional location)
561/// 2. .forge/*.db files
562/// 3. *.db in current directory
563/// 4. mirage.db or magellan.db in current directory
564fn auto_discover_db() -> Option<String> {
565    use std::path::Path;
566
567    // Search directories in priority order
568    let search_dirs = [".magellan", ".forge", "."];
569
570    for dir in &search_dirs {
571        if let Ok(entries) = std::fs::read_dir(dir) {
572            let mut db_files: Vec<_> = entries
573                .filter_map(|e| e.ok())
574                .filter(|e| {
575                    let path = e.path();
576                    path.extension().map(|ext| ext == "db").unwrap_or(false)
577                })
578                .map(|e| e.path())
579                .collect();
580
581            // Sort for deterministic results
582            db_files.sort();
583
584            // Return first match, preferring current Magellan/Mirage database names.
585            if let Some(preferred) = db_files.iter().find(|p| {
586                let name = p
587                    .file_stem()
588                    .map(|s| s.to_string_lossy())
589                    .unwrap_or_default();
590                name == "magellan" || name == "mirage"
591            }) {
592                return Some(preferred.to_string_lossy().to_string());
593            }
594
595            // Otherwise return first .db file
596            if let Some(first) = db_files.first() {
597                return Some(first.to_string_lossy().to_string());
598            }
599        }
600    }
601
602    // Check for specific filenames in current directory
603    let candidates = [
604        ".magellan/mirage.db",
605        ".magellan/magellan.db",
606        "mirage.db",
607        "magellan.db",
608        "graph.db",
609    ];
610    for name in &candidates {
611        if Path::new(name).exists() {
612            return Some(name.to_string());
613        }
614    }
615
616    None
617}
618
619/// Detect the git repository path from the database path
620///
621/// Starts from the db path and searches upward for .git directory.
622/// Falls back to current directory if not found.
623fn detect_repo_path(db_path: &str) -> std::path::PathBuf {
624    use std::path::Path;
625
626    let db_path = Path::new(db_path);
627
628    // Start from db path and search up for .git directory
629    let mut path = if db_path.is_absolute() {
630        db_path.to_path_buf()
631    } else {
632        std::env::current_dir()
633            .map(|cwd| cwd.join(db_path))
634            .unwrap_or_else(|_| db_path.to_path_buf())
635    };
636
637    // Search up the directory tree
638    while path.pop() {
639        let git_dir = path.join(".git");
640        if git_dir.exists() {
641            return path;
642        }
643    }
644
645    // Fallback to current directory
646    Path::new(".").to_path_buf()
647}
648
649// ============================================================================
650// Response Structs for JSON Output
651// ============================================================================
652
653/// Response for paths command
654#[derive(serde::Serialize)]
655struct PathsResponse {
656    function: String,
657    total_paths: usize,
658    error_paths: usize,
659    paths: Vec<PathSummary>,
660}
661
662/// LLM-optimized block representation with metadata
663#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
664struct PathBlock {
665    block_id: usize,
666    terminator: String,
667}
668
669/// Source location range for a path (to be populated in plan 07-02)
670#[derive(serde::Serialize)]
671struct SourceRange {
672    file_path: String,
673    start_line: usize,
674    end_line: usize,
675}
676
677/// Summary of a single path for JSON output (LLM-optimized)
678#[derive(serde::Serialize)]
679struct PathSummary {
680    path_id: String,
681    kind: String,
682    length: usize,
683    blocks: Vec<PathBlock>,
684    /// Human-readable summary (to be populated in plan 07-04)
685    summary: Option<String>,
686    /// Source range for the entire path (to be populated in plan 07-02)
687    source_range: Option<SourceRange>,
688}
689
690impl From<crate::cfg::Path> for PathSummary {
691    fn from(path: crate::cfg::Path) -> Self {
692        let length = path.len();
693        // Convert Vec<usize> block IDs to Vec<PathBlock> with placeholder terminator
694        // Full terminator info will be added in plan 07-02 when source locations are integrated
695        let blocks: Vec<PathBlock> = path
696            .blocks
697            .into_iter()
698            .map(|block_id| PathBlock {
699                block_id,
700                terminator: "Unknown".to_string(),
701            })
702            .collect();
703
704        Self {
705            path_id: path.path_id,
706            kind: format!("{:?}", path.kind),
707            length,
708            blocks,
709            summary: None,      // To be populated in plan 07-04
710            source_range: None, // To be populated in plan 07-02
711        }
712    }
713}
714
715impl PathSummary {
716    /// Create PathSummary with CFG data for source locations
717    /// This provides actual terminator types and source range information
718    pub fn from_with_cfg(path: crate::cfg::Path, cfg: &crate::cfg::Cfg) -> Self {
719        use crate::cfg::summarize_path;
720
721        // Generate natural language summary
722        let summary = Some(summarize_path(cfg, &path));
723
724        // Build PathBlock list with actual terminator types from CFG
725        let blocks: Vec<PathBlock> = path
726            .blocks
727            .iter()
728            .map(|&block_id| {
729                // Find the node in the CFG
730                let node_idx = cfg.node_indices().find(|&n| cfg[n].id == block_id);
731
732                let terminator = match node_idx {
733                    Some(idx) => format!("{:?}", cfg[idx].terminator),
734                    None => "Unknown".to_string(),
735                };
736
737                PathBlock {
738                    block_id,
739                    terminator,
740                }
741            })
742            .collect();
743
744        // Calculate source range from first and last blocks
745        let source_range = Self::calculate_source_range(&path, cfg);
746
747        let length = path.len();
748
749        Self {
750            path_id: path.path_id,
751            kind: format!("{:?}", path.kind),
752            length,
753            summary,
754            source_range,
755            blocks,
756        }
757    }
758
759    /// Calculate overall source range for a path
760    fn calculate_source_range(
761        path: &crate::cfg::Path,
762        cfg: &crate::cfg::Cfg,
763    ) -> Option<SourceRange> {
764        let first_loc = path
765            .blocks
766            .first()
767            .and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
768            .and_then(|idx| cfg[idx].source_location.clone());
769
770        let last_loc = path
771            .blocks
772            .last()
773            .and_then(|&bid| cfg.node_indices().find(|&n| cfg[n].id == bid))
774            .and_then(|idx| cfg[idx].source_location.clone());
775
776        match (first_loc, last_loc) {
777            (Some(first), Some(last)) => {
778                // Use first file_path, combine line ranges
779                Some(SourceRange {
780                    file_path: first.file_path.to_string_lossy().to_string(),
781                    start_line: first.start_line,
782                    end_line: last.end_line,
783                })
784            }
785            _ => None,
786        }
787    }
788}
789
790/// Response for dominators command
791#[derive(serde::Serialize)]
792struct DominanceResponse {
793    function: String,
794    kind: String, // "dominators" or "post-dominators"
795    root: Option<usize>,
796    dominance_tree: Vec<DominatorEntry>,
797    must_pass_through: Option<MustPassThroughResult>,
798}
799
800/// Entry in dominance tree for JSON output
801#[derive(serde::Serialize)]
802struct DominatorEntry {
803    block: usize,
804    immediate_dominator: Option<usize>,
805    dominated: Vec<usize>,
806}
807
808/// Result of must-pass-through query
809#[derive(serde::Serialize)]
810struct MustPassThroughResult {
811    block: usize,
812    must_pass: Vec<usize>,
813}
814
815/// Response for inter-procedural dominators command
816#[derive(serde::Serialize)]
817struct InterProceduralDominanceResponse {
818    /// Target function being analyzed
819    function: String,
820    /// Kind of analysis ("inter-procedural-dominators")
821    kind: String,
822    /// Number of dominating functions found
823    dominator_count: usize,
824    /// Functions that dominate the target (on all call paths)
825    dominators: Vec<String>,
826}
827
828/// Response for unreachable command
829#[derive(serde::Serialize)]
830struct UnreachableResponse {
831    function: String,
832    total_functions: usize,
833    functions_with_unreachable: usize,
834    unreachable_count: usize,
835    blocks: Vec<UnreachableBlock>,
836    /// Uncalled functions (only populated when --include-uncalled is set)
837    #[serde(skip_serializing_if = "Option::is_none")]
838    uncalled_functions: Option<Vec<DeadSymbolJson>>,
839}
840
841/// Incoming edge information for unreachable blocks
842#[derive(serde::Serialize, Clone)]
843struct IncomingEdge {
844    from_block: usize,
845    edge_type: String,
846}
847
848/// Unreachable block details for JSON output
849#[derive(serde::Serialize, Clone)]
850struct UnreachableBlock {
851    block_id: usize,
852    kind: String,
853    statements: Vec<String>,
854    terminator: String,
855    #[serde(skip_serializing_if = "Vec::is_empty")]
856    incoming_edges: Vec<IncomingEdge>,
857}
858
859/// Response for verify command
860#[derive(serde::Serialize)]
861struct VerifyResult {
862    path_id: String,
863    valid: bool,
864    found_in_cache: bool,
865    function_id: Option<i64>,
866    reason: String,
867    current_paths: usize,
868}
869
870/// Response for loops command
871#[derive(serde::Serialize)]
872struct LoopsResponse {
873    function: String,
874    loop_count: usize,
875    loops: Vec<LoopInfo>,
876}
877
878/// Information about a single natural loop
879#[derive(serde::Serialize)]
880struct LoopInfo {
881    header: usize,
882    back_edge_from: usize,
883    body_size: usize,
884    nesting_level: usize,
885    body_blocks: Vec<usize>,
886}
887
888/// Response for patterns command
889#[derive(serde::Serialize)]
890struct PatternsResponse {
891    function: String,
892    if_else_count: usize,
893    match_count: usize,
894    if_else_patterns: Vec<IfElseInfo>,
895    match_patterns: Vec<MatchInfo>,
896}
897
898/// Information about a single if/else pattern
899#[derive(serde::Serialize)]
900struct IfElseInfo {
901    condition_block: usize,
902    true_branch: usize,
903    false_branch: usize,
904    merge_point: Option<usize>,
905    has_else: bool,
906}
907
908/// Information about a single match pattern
909#[derive(serde::Serialize)]
910struct MatchInfo {
911    switch_block: usize,
912    branch_count: usize,
913    targets: Vec<usize>,
914    otherwise: usize,
915}
916
917/// Response for frontiers command
918#[derive(serde::Serialize)]
919struct FrontiersResponse {
920    function: String,
921    nodes_with_frontiers: usize,
922    frontiers: Vec<NodeFrontier>,
923}
924
925/// Information about a single node's dominance frontier
926#[derive(serde::Serialize)]
927struct NodeFrontier {
928    node: usize,
929    frontier_set: Vec<usize>,
930}
931
932/// Response for iterated frontier command
933#[derive(serde::Serialize)]
934struct IteratedFrontierResponse {
935    function: String,
936    iterated_frontier: Vec<usize>,
937}
938
939/// Response for block impact analysis (blast zone)
940#[derive(serde::Serialize)]
941struct BlockImpactResponse {
942    function: String,
943    block_id: usize,
944    reachable_blocks: Vec<usize>,
945    reachable_count: usize,
946    max_depth: usize,
947    has_cycles: bool,
948    #[serde(skip_serializing_if = "Option::is_none")]
949    forward_impact: Option<Vec<CallGraphSymbol>>,
950    #[serde(skip_serializing_if = "Option::is_none")]
951    backward_impact: Option<Vec<CallGraphSymbol>>,
952}
953
954/// Response for path impact analysis (blast zone)
955#[derive(serde::Serialize)]
956struct PathImpactResponse {
957    path_id: String,
958    path_length: usize,
959    unique_blocks_affected: Vec<usize>,
960    impact_count: usize,
961    #[serde(skip_serializing_if = "Option::is_none")]
962    forward_impact: Option<Vec<CallGraphSymbol>>,
963    #[serde(skip_serializing_if = "Option::is_none")]
964    backward_impact: Option<Vec<CallGraphSymbol>>,
965}
966
967/// Call graph symbol for impact analysis
968#[derive(Clone, serde::Serialize)]
969struct CallGraphSymbol {
970    #[serde(skip_serializing_if = "Option::is_none")]
971    symbol_id: Option<String>,
972    #[serde(skip_serializing_if = "Option::is_none")]
973    fqn: Option<String>,
974    file_path: String,
975    kind: String,
976}
977
978/// Response for hotspots command
979#[derive(serde::Serialize)]
980struct HotspotsResponse {
981    /// Entry point used for analysis
982    entry_point: String,
983    /// Total functions analyzed
984    total_functions: usize,
985    /// Hotspots found
986    hotspots: Vec<HotspotEntry>,
987    /// Analysis mode used
988    mode: String, // "intra-procedural" or "inter-procedural"
989}
990
991/// Single hotspot entry
992#[derive(serde::Serialize, Clone)]
993struct HotspotEntry {
994    /// Function name
995    function: String,
996    /// Risk score (higher = more risky)
997    risk_score: f64,
998    /// Path count (execution paths through this function)
999    path_count: usize,
1000    /// Dominance factor (SCC size or dominance level)
1001    dominance_factor: f64,
1002    /// Complexity metric (block count)
1003    complexity: usize,
1004    /// File path
1005    file_path: String,
1006}
1007
1008// ============================================================================
1009// Command Handlers (stubs for now)
1010// ============================================================================
1011
1012pub mod cmds {
1013    use super::*;
1014    use crate::output;
1015    use anyhow::Result;
1016
1017    pub fn status(_args: &StatusArgs, cli: &Cli) -> Result<()> {
1018        use crate::storage::MirageDb;
1019
1020        // Resolve database path
1021        let db_path = super::resolve_db_path(cli.db.clone())?;
1022
1023        // Open database
1024        let db = match MirageDb::open(&db_path) {
1025            Ok(db) => db,
1026            Err(e) => {
1027                // JSON-aware error handling with remediation
1028                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1029                    let error = output::JsonError::database_not_found(&db_path);
1030                    let wrapper = output::JsonResponse::new(error);
1031                    println!("{}", wrapper.to_json());
1032                    std::process::exit(output::EXIT_DATABASE);
1033                } else {
1034                    output::error(&format!("Failed to open database: {}", db_path));
1035                    output::error(&format!("Error details: {}", e));
1036                    output::info("Hint: Run 'magellan watch' to create the database");
1037                    std::process::exit(output::EXIT_DATABASE);
1038                }
1039            }
1040        };
1041
1042        // Query database statistics
1043        let status = db.status()?;
1044
1045        // Output based on format
1046        // VERIFIED: All three output formats (human/json/pretty) are implemented correctly
1047        // and follow Magellan's JsonResponse wrapper pattern for JSON outputs.
1048        match cli.output {
1049            OutputFormat::Human => {
1050                // Human-readable text format
1051                println!("Mirage Database Status:");
1052                println!(
1053                    "  Schema version: {} (Magellan: {})",
1054                    status.mirage_schema_version, status.magellan_schema_version
1055                );
1056                println!("  cfg_blocks: {}", status.cfg_blocks);
1057                // cfg_edges are computed in memory from terminators, not stored
1058                // cfg_paths requires explicit enumeration via 'mirage paths --function <name>'
1059                println!(
1060                    "  cfg_paths: {} (use 'mirage paths --function <name>' to enumerate)",
1061                    status.cfg_paths
1062                );
1063                println!("  cfg_dominators: {}", status.cfg_dominators);
1064            }
1065            OutputFormat::Json => {
1066                // Compact JSON
1067                let response = output::JsonResponse::new(status);
1068                println!("{}", response.to_json());
1069            }
1070            OutputFormat::Pretty => {
1071                // Formatted JSON with indentation
1072                let response = output::JsonResponse::new(status);
1073                println!("{}", response.to_pretty_json());
1074            }
1075        }
1076
1077        Ok(())
1078    }
1079
1080    pub fn paths(args: &PathsArgs, cli: &Cli) -> Result<()> {
1081        use crate::cfg::{
1082            enumerate_paths_incremental, get_or_enumerate_paths, PathKind, PathLimits,
1083        };
1084        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1085        use crate::storage::{get_function_hash_db, MirageDb};
1086
1087        // Resolve database path
1088        let db_path = super::resolve_db_path(cli.db.clone())?;
1089
1090        // Detect repository path for incremental mode
1091        let repo_path = detect_repo_path(&db_path);
1092
1093        // Handle incremental mode
1094        if args.incremental {
1095            let since = args
1096                .since
1097                .as_ref()
1098                .ok_or_else(|| anyhow::anyhow!("--since required with --incremental"))?;
1099
1100            // Open database for incremental mode
1101            let db = match MirageDb::open(&db_path) {
1102                Ok(db) => db,
1103                Err(_e) => {
1104                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1105                        let error = output::JsonError::database_not_found(&db_path);
1106                        let wrapper = output::JsonResponse::new(error);
1107                        println!("{}", wrapper.to_json());
1108                        std::process::exit(output::EXIT_DATABASE);
1109                    } else {
1110                        output::error(&format!("Failed to open database: {}", db_path));
1111                        output::info("Hint: Run 'magellan watch' to create the database");
1112                        std::process::exit(output::EXIT_DATABASE);
1113                    }
1114                }
1115            };
1116
1117            // Run incremental path enumeration
1118            let result = match enumerate_paths_incremental(
1119                &args.function,
1120                &db,
1121                &repo_path,
1122                since,
1123                args.max_length,
1124            ) {
1125                Ok(r) => r,
1126                Err(e) => {
1127                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1128                        let error = output::JsonError::new(
1129                            "IncrementalAnalysisError",
1130                            &format!("Incremental analysis failed: {}", e),
1131                            output::E_CFG_ERROR,
1132                        );
1133                        let wrapper = output::JsonResponse::new(error);
1134                        println!("{}", wrapper.to_json());
1135                        std::process::exit(output::EXIT_DATABASE);
1136                    } else {
1137                        output::error(&format!("Incremental analysis failed: {}", e));
1138                        std::process::exit(output::EXIT_DATABASE);
1139                    }
1140                }
1141            };
1142
1143            // Output results
1144            match cli.output {
1145                OutputFormat::Human => {
1146                    println!("Incremental path enumeration (since {}):", since);
1147                    println!("  Analyzed functions: {}", result.analyzed_functions);
1148                    println!("  Total paths: {}", result.paths.len());
1149
1150                    if args.show_errors {
1151                        let error_count = result
1152                            .paths
1153                            .iter()
1154                            .filter(|p| matches!(p.kind, PathKind::Error))
1155                            .count();
1156                        println!("  Error paths: {}", error_count);
1157                    }
1158
1159                    if !result.paths.is_empty() {
1160                        println!("\nPaths:");
1161                        for path in &result.paths {
1162                            if args.show_errors || !matches!(path.kind, PathKind::Error) {
1163                                println!("  {}", path);
1164                            }
1165                        }
1166                    }
1167                }
1168                OutputFormat::Json => {
1169                    let response = serde_json::json!({
1170                        "incremental": true,
1171                        "since": since,
1172                        "analyzed_functions": result.analyzed_functions,
1173                        "skipped_functions": result.skipped_functions,
1174                        "total_paths": result.paths.len(),
1175                        "paths": result.paths,
1176                    });
1177                    println!("{}", serde_json::to_string(&response)?);
1178                }
1179                OutputFormat::Pretty => {
1180                    let response = serde_json::json!({
1181                        "incremental": true,
1182                        "since": since,
1183                        "analyzed_functions": result.analyzed_functions,
1184                        "skipped_functions": result.skipped_functions,
1185                        "total_paths": result.paths.len(),
1186                        "paths": result.paths,
1187                    });
1188                    println!("{}", serde_json::to_string_pretty(&response)?);
1189                }
1190            }
1191
1192            return Ok(());
1193        }
1194
1195        // Standard path enumeration (non-incremental)
1196        // Open database
1197        let mut db = match MirageDb::open(&db_path) {
1198            Ok(db) => db,
1199            Err(_e) => {
1200                // JSON-aware error handling with remediation
1201                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1202                    let error = output::JsonError::database_not_found(&db_path);
1203                    let wrapper = output::JsonResponse::new(error);
1204                    println!("{}", wrapper.to_json());
1205                    std::process::exit(output::EXIT_DATABASE);
1206                } else {
1207                    output::error(&format!("Failed to open database: {}", db_path));
1208                    output::info("Hint: Run 'magellan watch' to create the database");
1209                    std::process::exit(output::EXIT_DATABASE);
1210                }
1211            }
1212        };
1213
1214        // Resolve function name/ID to function_id (with optional file filter)
1215        let function_id =
1216            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1217                Ok(id) => id,
1218                Err(_e) => {
1219                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1220                        let error = output::JsonError::function_not_found(&args.function);
1221                        let wrapper = output::JsonResponse::new(error);
1222                        println!("{}", wrapper.to_json());
1223                        std::process::exit(output::EXIT_DATABASE);
1224                    } else {
1225                        output::error(&format!(
1226                            "Function '{}' not found in database",
1227                            args.function
1228                        ));
1229                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1230                        std::process::exit(output::EXIT_DATABASE);
1231                    }
1232                }
1233            };
1234
1235        // Load CFG from database
1236        let cfg = match load_cfg_from_db(&db, function_id) {
1237            Ok(cfg) => cfg,
1238            Err(_e) => {
1239                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1240                    let error = output::JsonError::new(
1241                        "CgfLoadError",
1242                        &format!("Failed to load CFG for function '{}'", args.function),
1243                        output::E_CFG_ERROR,
1244                    );
1245                    let wrapper = output::JsonResponse::new(error);
1246                    println!("{}", wrapper.to_json());
1247                    std::process::exit(output::EXIT_DATABASE);
1248                } else {
1249                    output::error(&format!(
1250                        "Failed to load CFG for function '{}'",
1251                        args.function
1252                    ));
1253                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
1254                    std::process::exit(output::EXIT_DATABASE);
1255                }
1256            }
1257        };
1258
1259        // Build path limits based on args
1260        let mut limits = PathLimits::default();
1261        if let Some(max_length) = args.max_length {
1262            limits = limits.with_max_length(max_length);
1263        }
1264
1265        // Enumerate paths (backend-agnostic)
1266        // For SQLite backend: use get_or_enumerate_paths for caching
1267        let mut paths = if db.is_sqlite() {
1268            // SQLite backend: use caching layer
1269            let function_hash = match get_function_hash_db(&db, function_id) {
1270                Some(hash) => hash,
1271                None => {
1272                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1273                        let error = output::JsonError::new(
1274                            "HashNotFound",
1275                            &format!("Function hash not found for '{}'", args.function),
1276                            output::E_CFG_ERROR,
1277                        );
1278                        let wrapper = output::JsonResponse::new(error);
1279                        println!("{}", wrapper.to_json());
1280                        std::process::exit(output::EXIT_DATABASE);
1281                    } else {
1282                        output::error(&format!("Function hash not found for '{}'", args.function));
1283                        output::info(
1284                            "The function data may be incomplete. Try re-running 'magellan watch'",
1285                        );
1286                        std::process::exit(output::EXIT_DATABASE);
1287                    }
1288                }
1289            };
1290
1291            get_or_enumerate_paths(&cfg, function_id, &function_hash, &limits, db.conn_mut()?)
1292                .map_err(|e| anyhow::anyhow!("Path enumeration failed: {}", e))?
1293        } else {
1294            // Native-v3 backend: enumerate directly without caching
1295            // Magellan manages its own caching
1296            crate::cfg::enumerate_paths(&cfg, &limits)
1297        };
1298
1299        // Filter to error paths if requested
1300        if args.show_errors {
1301            paths.retain(|p| p.kind == PathKind::Error);
1302        }
1303
1304        // Sort by coverage if requested (highest total hit count first)
1305        if args.by_coverage {
1306            let coverage_map: std::collections::HashMap<i64, i64> = db
1307                .conn()
1308                .ok()
1309                .and_then(|conn| {
1310                    let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
1311                           WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
1312                    let mut stmt = conn.prepare(sql).ok()?;
1313                    let rows = stmt.query_map([function_id], |row| {
1314                        Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1315                    });
1316                    let mut map = std::collections::HashMap::new();
1317                    if let Ok(iter) = rows {
1318                        for item in iter {
1319                            if let Ok((block_id, hit_count)) = item {
1320                                map.insert(block_id, hit_count);
1321                            }
1322                        }
1323                    }
1324                    if map.is_empty() {
1325                        None
1326                    } else {
1327                        Some(map)
1328                    }
1329                })
1330                .unwrap_or_default();
1331
1332            // Build graph node index -> hit_count lookup via db_id
1333            let node_hits: std::collections::HashMap<usize, i64> = cfg
1334                .node_indices()
1335                .filter_map(|idx| {
1336                    cfg.node_weight(idx).and_then(|b| {
1337                        b.db_id
1338                            .and_then(|db_id| coverage_map.get(&db_id).copied())
1339                            .map(|hits| (b.id, hits))
1340                    })
1341                })
1342                .collect();
1343
1344            paths.sort_by(|a, b| {
1345                let total_a: i64 = a
1346                    .blocks
1347                    .iter()
1348                    .map(|bid| node_hits.get(bid).copied().unwrap_or(0))
1349                    .sum();
1350                let total_b: i64 = b
1351                    .blocks
1352                    .iter()
1353                    .map(|bid| node_hits.get(bid).copied().unwrap_or(0))
1354                    .sum();
1355                total_b.cmp(&total_a) // descending
1356            });
1357        }
1358
1359        // Count error paths for reporting
1360        let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
1361
1362        // Format output based on cli.output
1363        match cli.output {
1364            OutputFormat::Human => {
1365                // Human-readable text format
1366                println!("Function: {}", args.function);
1367                println!("Total paths: {}", paths.len());
1368                if args.show_errors {
1369                    println!("(Showing error paths only)");
1370                } else {
1371                    println!("Error paths: {}", error_count);
1372                }
1373                println!();
1374
1375                if paths.is_empty() {
1376                    output::info("No paths found");
1377                    return Ok(());
1378                }
1379
1380                for (i, path) in paths.iter().enumerate() {
1381                    println!("Path {}: {}", i + 1, path.path_id);
1382                    println!("  Kind: {:?}", path.kind);
1383                    println!("  Length: {} blocks", path.len());
1384                    if args.with_blocks {
1385                        println!(
1386                            "  Blocks: {}",
1387                            path.blocks
1388                                .iter()
1389                                .map(|id| id.to_string())
1390                                .collect::<Vec<_>>()
1391                                .join(" -> ")
1392                        );
1393                    }
1394                    println!();
1395                }
1396            }
1397            OutputFormat::Json => {
1398                // Compact JSON with source locations from CFG
1399                let response = PathsResponse {
1400                    function: args.function.clone(),
1401                    total_paths: paths.len(),
1402                    error_paths: error_count,
1403                    paths: paths
1404                        .iter()
1405                        .map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
1406                        .collect(),
1407                };
1408                let wrapper = output::JsonResponse::new(response);
1409                println!("{}", wrapper.to_json());
1410            }
1411            OutputFormat::Pretty => {
1412                // Formatted JSON with indentation and source locations from CFG
1413                let response = PathsResponse {
1414                    function: args.function.clone(),
1415                    total_paths: paths.len(),
1416                    error_paths: error_count,
1417                    paths: paths
1418                        .iter()
1419                        .map(|p| PathSummary::from_with_cfg(p.clone(), &cfg))
1420                        .collect(),
1421                };
1422                let wrapper = output::JsonResponse::new(response);
1423                println!("{}", wrapper.to_pretty_json());
1424            }
1425        }
1426
1427        Ok(())
1428    }
1429
1430    pub fn cfg(args: &CfgArgs, cli: &Cli) -> Result<()> {
1431        use crate::cfg::{export_dot, export_json, CFGExport};
1432        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1433        use crate::storage::MirageDb;
1434
1435        // Resolve database path
1436        let db_path = super::resolve_db_path(cli.db.clone())?;
1437
1438        // Open database (follows status command pattern for error handling)
1439        let db = match MirageDb::open(&db_path) {
1440            Ok(db) => db,
1441            Err(_e) => {
1442                // JSON-aware error handling with remediation
1443                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1444                    let error = output::JsonError::database_not_found(&db_path);
1445                    let wrapper = output::JsonResponse::new(error);
1446                    println!("{}", wrapper.to_json());
1447                    std::process::exit(output::EXIT_DATABASE);
1448                } else {
1449                    output::error(&format!("Failed to open database: {}", db_path));
1450                    output::info("Hint: Run 'magellan watch' to create the database");
1451                    std::process::exit(output::EXIT_DATABASE);
1452                }
1453            }
1454        };
1455
1456        // Resolve function name/ID to function_id (with optional file filter)
1457        let function_id =
1458            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1459                Ok(id) => id,
1460                Err(_e) => {
1461                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1462                        let error = output::JsonError::function_not_found(&args.function);
1463                        let wrapper = output::JsonResponse::new(error);
1464                        println!("{}", wrapper.to_json());
1465                        std::process::exit(output::EXIT_DATABASE);
1466                    } else {
1467                        output::error(&format!(
1468                            "Function '{}' not found in database",
1469                            args.function
1470                        ));
1471                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1472                        std::process::exit(output::EXIT_DATABASE);
1473                    }
1474                }
1475            };
1476
1477        // Load CFG from database
1478        let cfg = match load_cfg_from_db(&db, function_id) {
1479            Ok(cfg) => cfg,
1480            Err(_e) => {
1481                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1482                    let error = output::JsonError::new(
1483                        "CgfLoadError",
1484                        &format!("Failed to load CFG for function '{}'", args.function),
1485                        output::E_CFG_ERROR,
1486                    );
1487                    let wrapper = output::JsonResponse::new(error);
1488                    println!("{}", wrapper.to_json());
1489                    std::process::exit(output::EXIT_DATABASE);
1490                } else {
1491                    output::error(&format!(
1492                        "Failed to load CFG for function '{}'",
1493                        args.function
1494                    ));
1495                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
1496                    std::process::exit(output::EXIT_DATABASE);
1497                }
1498            }
1499        };
1500
1501        // Query coverage data (Magellan v3.1.6+ tables; graceful fallback if absent)
1502        let coverage: Option<std::collections::HashMap<i64, i64>> =
1503            db.conn().ok().and_then(|conn| {
1504                let sql = "SELECT block_id, hit_count FROM cfg_block_coverage \
1505                       WHERE block_id IN (SELECT id FROM cfg_blocks WHERE function_id = ?1)";
1506                let mut stmt = conn.prepare(sql).ok()?;
1507                let rows = stmt.query_map([function_id], |row| {
1508                    Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
1509                });
1510                let mut map = std::collections::HashMap::new();
1511                if let Ok(iter) = rows {
1512                    for item in iter {
1513                        if let Ok((block_id, hit_count)) = item {
1514                            map.insert(block_id, hit_count);
1515                        }
1516                    }
1517                }
1518                if map.is_empty() {
1519                    None
1520                } else {
1521                    Some(map)
1522                }
1523            });
1524
1525        // Determine output format (args.format overrides cli.output)
1526        let format = args.format.unwrap_or(match cli.output {
1527            OutputFormat::Human => CfgFormat::Human,
1528            OutputFormat::Json => CfgFormat::Json,
1529            OutputFormat::Pretty => CfgFormat::Json,
1530        });
1531
1532        match format {
1533            CfgFormat::Human | CfgFormat::Dot => {
1534                // Both Human and Dot use DOT format
1535                let dot = export_dot(&cfg);
1536                println!("{}", dot);
1537            }
1538            CfgFormat::Json => {
1539                // Export to JSON and wrap in JsonResponse for consistency
1540                let export: CFGExport = export_json(&cfg, &args.function, coverage.as_ref());
1541                let response = output::JsonResponse::new(export);
1542
1543                match cli.output {
1544                    OutputFormat::Json => println!("{}", response.to_json()),
1545                    OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
1546                    OutputFormat::Human => println!("{}", response.to_pretty_json()),
1547                }
1548            }
1549        }
1550
1551        Ok(())
1552    }
1553
1554    /// Helper to create a test CFG for demonstration
1555    ///
1556    /// This will be replaced with database loading in future plans
1557    /// when MIR extraction (02-01) is complete.
1558    pub(crate) fn create_test_cfg() -> crate::cfg::Cfg {
1559        use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
1560        use petgraph::graph::DiGraph;
1561        let mut g = DiGraph::new();
1562
1563        let b0 = g.add_node(BasicBlock {
1564            id: 0,
1565            db_id: None,
1566            kind: BlockKind::Entry,
1567            statements: vec!["let x = 1".to_string()],
1568            terminator: Terminator::Goto { target: 1 },
1569            source_location: None,
1570            coord_x: 0,
1571            coord_y: 0,
1572            coord_z: 0,
1573        });
1574
1575        let b1 = g.add_node(BasicBlock {
1576            id: 1,
1577            db_id: None,
1578            kind: BlockKind::Normal,
1579            statements: vec!["if x > 0".to_string()],
1580            terminator: Terminator::SwitchInt {
1581                targets: vec![2],
1582                otherwise: 3,
1583            },
1584            source_location: None,
1585            coord_x: 1,
1586            coord_y: 0,
1587            coord_z: 1,
1588        });
1589
1590        let b2 = g.add_node(BasicBlock {
1591            id: 2,
1592            db_id: None,
1593            kind: BlockKind::Exit,
1594            statements: vec!["return true".to_string()],
1595            terminator: Terminator::Return,
1596            source_location: None,
1597            coord_x: 2,
1598            coord_y: 0,
1599            coord_z: 2,
1600        });
1601
1602        let b3 = g.add_node(BasicBlock {
1603            id: 3,
1604            db_id: None,
1605            kind: BlockKind::Exit,
1606            statements: vec!["return false".to_string()],
1607            terminator: Terminator::Return,
1608            source_location: None,
1609            coord_x: 2,
1610            coord_y: 0,
1611            coord_z: 3,
1612        });
1613
1614        g.add_edge(b0, b1, EdgeType::Fallthrough);
1615        g.add_edge(b1, b2, EdgeType::TrueBranch);
1616        g.add_edge(b1, b3, EdgeType::FalseBranch);
1617
1618        g
1619    }
1620
1621    pub fn dominators(args: &DominatorsArgs, cli: &Cli) -> Result<()> {
1622        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
1623        use crate::cfg::{DominatorTree, PostDominatorTree};
1624        use crate::storage::MirageDb;
1625
1626        // Resolve database path
1627        let db_path = super::resolve_db_path(cli.db.clone())?;
1628
1629        // Handle inter-procedural mode using call graph dominance
1630        if args.inter_procedural {
1631            return inter_procedural_dominators(args, cli, &db_path);
1632        }
1633
1634        // Open database (follows status command pattern for error handling)
1635        let db = match MirageDb::open(&db_path) {
1636            Ok(db) => db,
1637            Err(_e) => {
1638                // JSON-aware error handling with remediation
1639                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1640                    let error = output::JsonError::database_not_found(&db_path);
1641                    let wrapper = output::JsonResponse::new(error);
1642                    println!("{}", wrapper.to_json());
1643                    std::process::exit(output::EXIT_DATABASE);
1644                } else {
1645                    output::error(&format!("Failed to open database: {}", db_path));
1646                    output::info("Hint: Run 'magellan watch' to create the database");
1647                    std::process::exit(output::EXIT_DATABASE);
1648                }
1649            }
1650        };
1651
1652        // Resolve function name/ID to function_id (with optional file filter)
1653        let function_id =
1654            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
1655                Ok(id) => id,
1656                Err(_e) => {
1657                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1658                        let error = output::JsonError::function_not_found(&args.function);
1659                        let wrapper = output::JsonResponse::new(error);
1660                        println!("{}", wrapper.to_json());
1661                        std::process::exit(output::EXIT_DATABASE);
1662                    } else {
1663                        output::error(&format!(
1664                            "Function '{}' not found in database",
1665                            args.function
1666                        ));
1667                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
1668                        std::process::exit(output::EXIT_DATABASE);
1669                    }
1670                }
1671            };
1672
1673        // Load CFG from database
1674        let cfg = match load_cfg_from_db(&db, function_id) {
1675            Ok(cfg) => cfg,
1676            Err(_e) => {
1677                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1678                    let error = output::JsonError::new(
1679                        "CgfLoadError",
1680                        &format!("Failed to load CFG for function '{}'", args.function),
1681                        output::E_CFG_ERROR,
1682                    );
1683                    let wrapper = output::JsonResponse::new(error);
1684                    println!("{}", wrapper.to_json());
1685                    std::process::exit(output::EXIT_DATABASE);
1686                } else {
1687                    output::error(&format!(
1688                        "Failed to load CFG for function '{}'",
1689                        args.function
1690                    ));
1691                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
1692                    std::process::exit(output::EXIT_DATABASE);
1693                }
1694            }
1695        };
1696
1697        // Compute dominator tree based on args.post flag
1698        if args.post {
1699            // Post-dominator analysis
1700            let post_dom_tree = match PostDominatorTree::new(&cfg) {
1701                Some(tree) => tree,
1702                None => {
1703                    output::error(
1704                        "Could not compute post-dominator tree (CFG may have no exit blocks)",
1705                    );
1706                    std::process::exit(1);
1707                }
1708            };
1709
1710            // Handle must-pass-through query if specified
1711            if let Some(ref block_id_str) = args.must_pass_through {
1712                match block_id_str.parse::<usize>() {
1713                    Ok(block_id) => {
1714                        // Find NodeIndex for this block
1715                        let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
1716
1717                        let target_node = match target_node {
1718                            Some(node) => node,
1719                            None => {
1720                                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1721                                    let error = output::JsonError::block_not_found(block_id);
1722                                    let wrapper = output::JsonResponse::new(error);
1723                                    println!("{}", wrapper.to_json());
1724                                    std::process::exit(1);
1725                                } else {
1726                                    output::error(&format!("Block {} not found in CFG", block_id));
1727                                    std::process::exit(1);
1728                                }
1729                            }
1730                        };
1731
1732                        // Find all nodes post-dominated by this block
1733                        let must_pass: Vec<usize> = cfg
1734                            .node_indices()
1735                            .filter(|&n| post_dom_tree.post_dominates(target_node, n))
1736                            .map(|n| cfg[n].id)
1737                            .collect();
1738
1739                        // Output based on format
1740                        match cli.output {
1741                            OutputFormat::Human => {
1742                                println!("Function: {}", args.function);
1743                                println!(
1744                                    "Post-Dominator Query: Blocks post-dominated by {}",
1745                                    block_id
1746                                );
1747                                println!("Count: {}", must_pass.len());
1748                                println!();
1749                                if must_pass.is_empty() {
1750                                    output::info("No blocks are post-dominated by this block");
1751                                } else {
1752                                    println!("Blocks that must pass through {}:", block_id);
1753                                    for id in &must_pass {
1754                                        println!("  - Block {}", id);
1755                                    }
1756                                }
1757                            }
1758                            OutputFormat::Json | OutputFormat::Pretty => {
1759                                let response = DominanceResponse {
1760                                    function: args.function.clone(),
1761                                    kind: "post-dominators".to_string(),
1762                                    root: Some(cfg[post_dom_tree.root()].id),
1763                                    dominance_tree: vec![],
1764                                    must_pass_through: Some(MustPassThroughResult {
1765                                        block: block_id,
1766                                        must_pass,
1767                                    }),
1768                                };
1769                                let wrapper = output::JsonResponse::new(response);
1770                                match cli.output {
1771                                    OutputFormat::Json => println!("{}", wrapper.to_json()),
1772                                    OutputFormat::Pretty => {
1773                                        println!("{}", wrapper.to_pretty_json())
1774                                    }
1775                                    _ => unreachable!(),
1776                                }
1777                            }
1778                        }
1779                        return Ok(());
1780                    }
1781                    Err(_) => {
1782                        output::error(&format!("Invalid block ID: {}", block_id_str));
1783                        std::process::exit(1);
1784                    }
1785                }
1786            }
1787
1788            // Build dominance tree for output
1789            let dominance_tree: Vec<DominatorEntry> = cfg
1790                .node_indices()
1791                .map(|node| {
1792                    let block = cfg[node].id;
1793                    let immediate_dominator = post_dom_tree
1794                        .immediate_post_dominator(node)
1795                        .map(|n| cfg[n].id);
1796                    let dominated: Vec<usize> = post_dom_tree
1797                        .children(node)
1798                        .iter()
1799                        .map(|&n| cfg[n].id)
1800                        .collect();
1801                    DominatorEntry {
1802                        block,
1803                        immediate_dominator,
1804                        dominated,
1805                    }
1806                })
1807                .collect();
1808
1809            // Format output
1810            match cli.output {
1811                OutputFormat::Human => {
1812                    println!("Function: {}", args.function);
1813                    println!(
1814                        "Post-Dominator Tree (root: {})",
1815                        cfg[post_dom_tree.root()].id
1816                    );
1817                    println!();
1818
1819                    // Print tree structure
1820                    print_dominator_tree_human(
1821                        &cfg,
1822                        post_dom_tree.as_dominator_tree(),
1823                        post_dom_tree.root(),
1824                        0,
1825                        true,
1826                    );
1827                }
1828                OutputFormat::Json | OutputFormat::Pretty => {
1829                    let response = DominanceResponse {
1830                        function: args.function.clone(),
1831                        kind: "post-dominators".to_string(),
1832                        root: Some(cfg[post_dom_tree.root()].id),
1833                        dominance_tree,
1834                        must_pass_through: None,
1835                    };
1836                    let wrapper = output::JsonResponse::new(response);
1837                    match cli.output {
1838                        OutputFormat::Json => println!("{}", wrapper.to_json()),
1839                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
1840                        _ => unreachable!(),
1841                    }
1842                }
1843            }
1844        } else {
1845            // Regular dominator analysis
1846            let dom_tree = match DominatorTree::new(&cfg) {
1847                Some(tree) => tree,
1848                None => {
1849                    output::error("Could not compute dominator tree (CFG may have no entry block)");
1850                    std::process::exit(1);
1851                }
1852            };
1853
1854            // Handle must-pass-through query if specified
1855            if let Some(ref block_id_str) = args.must_pass_through {
1856                match block_id_str.parse::<usize>() {
1857                    Ok(block_id) => {
1858                        // Find NodeIndex for this block
1859                        let target_node = cfg.node_indices().find(|&n| cfg[n].id == block_id);
1860
1861                        let target_node = match target_node {
1862                            Some(node) => node,
1863                            None => {
1864                                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
1865                                    let error = output::JsonError::block_not_found(block_id);
1866                                    let wrapper = output::JsonResponse::new(error);
1867                                    println!("{}", wrapper.to_json());
1868                                    std::process::exit(1);
1869                                } else {
1870                                    output::error(&format!("Block {} not found in CFG", block_id));
1871                                    std::process::exit(1);
1872                                }
1873                            }
1874                        };
1875
1876                        // Find all nodes dominated by this block
1877                        let must_pass: Vec<usize> = cfg
1878                            .node_indices()
1879                            .filter(|&n| dom_tree.dominates(target_node, n))
1880                            .map(|n| cfg[n].id)
1881                            .collect();
1882
1883                        // Output based on format
1884                        match cli.output {
1885                            OutputFormat::Human => {
1886                                println!("Function: {}", args.function);
1887                                println!("Dominator Query: Blocks dominated by {}", block_id);
1888                                println!("Count: {}", must_pass.len());
1889                                println!();
1890                                if must_pass.is_empty() {
1891                                    output::info("No blocks are dominated by this block");
1892                                } else {
1893                                    println!("Blocks that must pass through {}:", block_id);
1894                                    for id in &must_pass {
1895                                        println!("  - Block {}", id);
1896                                    }
1897                                }
1898                            }
1899                            OutputFormat::Json | OutputFormat::Pretty => {
1900                                let response = DominanceResponse {
1901                                    function: args.function.clone(),
1902                                    kind: "dominators".to_string(),
1903                                    root: Some(cfg[dom_tree.root()].id),
1904                                    dominance_tree: vec![],
1905                                    must_pass_through: Some(MustPassThroughResult {
1906                                        block: block_id,
1907                                        must_pass,
1908                                    }),
1909                                };
1910                                let wrapper = output::JsonResponse::new(response);
1911                                match cli.output {
1912                                    OutputFormat::Json => println!("{}", wrapper.to_json()),
1913                                    OutputFormat::Pretty => {
1914                                        println!("{}", wrapper.to_pretty_json())
1915                                    }
1916                                    _ => unreachable!(),
1917                                }
1918                            }
1919                        }
1920                        return Ok(());
1921                    }
1922                    Err(_) => {
1923                        output::error(&format!("Invalid block ID: {}", block_id_str));
1924                        std::process::exit(1);
1925                    }
1926                }
1927            }
1928
1929            // Build dominance tree for output
1930            let dominance_tree: Vec<DominatorEntry> = cfg
1931                .node_indices()
1932                .map(|node| {
1933                    let block = cfg[node].id;
1934                    let immediate_dominator = dom_tree.immediate_dominator(node).map(|n| cfg[n].id);
1935                    let dominated: Vec<usize> =
1936                        dom_tree.children(node).iter().map(|&n| cfg[n].id).collect();
1937                    DominatorEntry {
1938                        block,
1939                        immediate_dominator,
1940                        dominated,
1941                    }
1942                })
1943                .collect();
1944
1945            // Format output
1946            match cli.output {
1947                OutputFormat::Human => {
1948                    println!("Function: {}", args.function);
1949                    println!("Dominator Tree (root: {})", cfg[dom_tree.root()].id);
1950                    println!();
1951
1952                    // Print tree structure
1953                    print_dominator_tree_human(&cfg, &dom_tree, dom_tree.root(), 0, false);
1954                }
1955                OutputFormat::Json | OutputFormat::Pretty => {
1956                    let response = DominanceResponse {
1957                        function: args.function.clone(),
1958                        kind: "dominators".to_string(),
1959                        root: Some(cfg[dom_tree.root()].id),
1960                        dominance_tree,
1961                        must_pass_through: None,
1962                    };
1963                    let wrapper = output::JsonResponse::new(response);
1964                    match cli.output {
1965                        OutputFormat::Json => println!("{}", wrapper.to_json()),
1966                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
1967                        _ => unreachable!(),
1968                    }
1969                }
1970            }
1971        }
1972
1973        Ok(())
1974    }
1975
1976    /// Helper to print dominator tree in human-readable format
1977    fn print_dominator_tree_human(
1978        cfg: &crate::cfg::Cfg,
1979        dom_tree: &crate::cfg::DominatorTree,
1980        node: petgraph::graph::NodeIndex,
1981        depth: usize,
1982        is_post: bool,
1983    ) {
1984        let indent = "  ".repeat(depth);
1985        let block_id = cfg[node].id;
1986        let kind_label = if is_post {
1987            "post-dominator"
1988        } else {
1989            "dominator"
1990        };
1991
1992        println!("{}Block {} ({})", indent, block_id, kind_label);
1993
1994        for &child in dom_tree.children(node) {
1995            print_dominator_tree_human(cfg, dom_tree, child, depth + 1, is_post);
1996        }
1997    }
1998
1999    /// Helper to print post-dominator tree in human-readable format
2000    fn print_post_dominator_tree_human(
2001        cfg: &crate::cfg::Cfg,
2002        post_dom_tree: &crate::cfg::PostDominatorTree,
2003        node: petgraph::graph::NodeIndex,
2004        depth: usize,
2005    ) {
2006        let indent = "  ".repeat(depth);
2007        let block_id = cfg[node].id;
2008
2009        println!("{}Block {} (post-dominator)", indent, block_id);
2010
2011        for &child in post_dom_tree.children(node) {
2012            print_post_dominator_tree_human(cfg, post_dom_tree, child, depth + 1);
2013        }
2014    }
2015
2016    /// Inter-procedural dominance analysis using call graph condensation
2017    ///
2018    /// Analyzes which functions dominate other functions in the call graph.
2019    /// Function A dominates Function B if ALL paths from entry to B must go through A.
2020    fn inter_procedural_dominators(args: &DominatorsArgs, cli: &Cli, db_path: &str) -> Result<()> {
2021        use crate::analysis::MagellanBridge;
2022        use std::collections::{HashMap, HashSet};
2023
2024        // Try to open Magellan database
2025        let bridge = match MagellanBridge::open(db_path) {
2026            Ok(b) => b,
2027            Err(e) => {
2028                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2029                    let error = output::JsonError::new(
2030                        "MagellanUnavailable",
2031                        &format!("Magellan database not available: {}", e),
2032                        "Run 'magellan watch' to build the call graph",
2033                    );
2034                    let wrapper = output::JsonResponse::new(error);
2035                    println!("{}", wrapper.to_json());
2036                    std::process::exit(output::EXIT_DATABASE);
2037                } else {
2038                    output::error(&format!("Magellan database not available: {}", e));
2039                    output::info("Hint: Run 'magellan watch' to build the call graph");
2040                    std::process::exit(output::EXIT_DATABASE);
2041                }
2042            }
2043        };
2044
2045        // Condense the call graph to get a DAG of SCCs
2046        let condensed = match bridge.condense_call_graph() {
2047            Ok(c) => c,
2048            Err(e) => {
2049                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2050                    let error = output::JsonError::new(
2051                        "CondensationError",
2052                        &format!("Failed to condense call graph: {}", e),
2053                        "Ensure the call graph is properly built",
2054                    );
2055                    let wrapper = output::JsonResponse::new(error);
2056                    println!("{}", wrapper.to_json());
2057                    std::process::exit(output::EXIT_DATABASE);
2058                } else {
2059                    output::error(&format!("Failed to condense call graph: {}", e));
2060                    output::info("Hint: Ensure the call graph is properly built");
2061                    std::process::exit(output::EXIT_DATABASE);
2062                }
2063            }
2064        };
2065
2066        // Build adjacency list from condensation edges (for reachability analysis)
2067        let mut adjacency: HashMap<i64, Vec<i64>> = HashMap::new();
2068        for &(from_id, to_id) in &condensed.graph.edges {
2069            adjacency.entry(from_id).or_default().push(to_id);
2070        }
2071
2072        // Map symbols to their SCC IDs
2073        let mut symbol_to_scc: HashMap<String, i64> = HashMap::new();
2074        let mut scc_members: HashMap<i64, Vec<String>> = HashMap::new();
2075
2076        for supernode in &condensed.graph.supernodes {
2077            let scc_id = supernode.id;
2078            for member in &supernode.members {
2079                if let Some(fqn) = &member.fqn {
2080                    symbol_to_scc.insert(fqn.clone(), scc_id);
2081                    scc_members.entry(scc_id).or_default().push(fqn.clone());
2082                }
2083            }
2084        }
2085
2086        // Find all functions that dominate the target function
2087        // In a DAG, functions in upstream SCCs dominate functions in downstream SCCs
2088        let mut dominating_functions: Vec<String> = Vec::new();
2089
2090        if let Some(&target_scc_id) = symbol_to_scc.get(&args.function) {
2091            // Find all SCCs that can reach the target SCC
2092            for (&scc_id, _) in &scc_members {
2093                if scc_id != target_scc_id {
2094                    let mut visited = HashSet::new();
2095                    if can_reach_scc(scc_id, target_scc_id, &adjacency, &mut visited) {
2096                        // Add all members of this SCC as dominators
2097                        if let Some(members) = scc_members.get(&scc_id) {
2098                            dominating_functions.extend(members.clone());
2099                        }
2100                    }
2101                }
2102            }
2103        }
2104
2105        // Sort for consistent output
2106        dominating_functions.sort();
2107
2108        // Format output
2109        match cli.output {
2110            OutputFormat::Human => {
2111                output::header(&format!("Inter-procedural Dominators: {}", args.function));
2112                output::info("Functions that must execute before this function can be reached");
2113                println!();
2114
2115                if dominating_functions.is_empty() {
2116                    println!(
2117                        "No dominators found (this may be an entry point or not in call graph)"
2118                    );
2119                } else {
2120                    println!(
2121                        "Found {} dominating function(s):",
2122                        dominating_functions.len()
2123                    );
2124                    println!();
2125                    for (i, dominator) in dominating_functions.iter().enumerate() {
2126                        println!("{}. {}", i + 1, dominator);
2127                    }
2128                    println!();
2129                    output::info("These functions are on all call paths to the target");
2130                }
2131            }
2132            OutputFormat::Json => {
2133                let response = InterProceduralDominanceResponse {
2134                    function: args.function.clone(),
2135                    kind: "inter-procedural-dominators".to_string(),
2136                    dominator_count: dominating_functions.len(),
2137                    dominators: dominating_functions.clone(),
2138                };
2139                let wrapper = output::JsonResponse::new(response);
2140                println!("{}", wrapper.to_json());
2141            }
2142            OutputFormat::Pretty => {
2143                let response = InterProceduralDominanceResponse {
2144                    function: args.function.clone(),
2145                    kind: "inter-procedural-dominators".to_string(),
2146                    dominator_count: dominating_functions.len(),
2147                    dominators: dominating_functions.clone(),
2148                };
2149                let wrapper = output::JsonResponse::new(response);
2150                println!("{}", wrapper.to_pretty_json());
2151            }
2152        }
2153
2154        Ok(())
2155    }
2156
2157    /// Check if SCC `from` can reach SCC `to` in the condensation DAG
2158    fn can_reach_scc(
2159        from: i64,
2160        to: i64,
2161        adjacency: &std::collections::HashMap<i64, Vec<i64>>,
2162        visited: &mut std::collections::HashSet<i64>,
2163    ) -> bool {
2164        if from == to {
2165            return true;
2166        }
2167        if visited.contains(&from) {
2168            return false;
2169        }
2170        visited.insert(from);
2171
2172        if let Some(neighbors) = adjacency.get(&from) {
2173            for &neighbor in neighbors {
2174                if can_reach_scc(neighbor, to, adjacency, visited) {
2175                    return true;
2176                }
2177            }
2178        }
2179        false
2180    }
2181
2182    pub fn loops(args: &LoopsArgs, cli: &Cli) -> Result<()> {
2183        use crate::cfg::detect_natural_loops;
2184        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
2185        use crate::storage::MirageDb;
2186
2187        // Resolve database path
2188        let db_path = super::resolve_db_path(cli.db.clone())?;
2189
2190        // Open database (follows status command pattern for error handling)
2191        let db = match MirageDb::open(&db_path) {
2192            Ok(db) => db,
2193            Err(_e) => {
2194                // JSON-aware error handling with remediation
2195                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2196                    let error = output::JsonError::database_not_found(&db_path);
2197                    let wrapper = output::JsonResponse::new(error);
2198                    println!("{}", wrapper.to_json());
2199                    std::process::exit(output::EXIT_DATABASE);
2200                } else {
2201                    output::error(&format!("Failed to open database: {}", db_path));
2202                    output::info("Hint: Run 'magellan watch' to create the database");
2203                    std::process::exit(output::EXIT_DATABASE);
2204                }
2205            }
2206        };
2207
2208        // Resolve function name/ID to function_id (with optional file filter)
2209        let function_id =
2210            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
2211                Ok(id) => id,
2212                Err(_e) => {
2213                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2214                        let error = output::JsonError::function_not_found(&args.function);
2215                        let wrapper = output::JsonResponse::new(error);
2216                        println!("{}", wrapper.to_json());
2217                        std::process::exit(output::EXIT_DATABASE);
2218                    } else {
2219                        output::error(&format!(
2220                            "Function '{}' not found in database",
2221                            args.function
2222                        ));
2223                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
2224                        std::process::exit(output::EXIT_DATABASE);
2225                    }
2226                }
2227            };
2228
2229        // Load CFG from database
2230        let cfg = match load_cfg_from_db(&db, function_id) {
2231            Ok(cfg) => cfg,
2232            Err(_e) => {
2233                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2234                    let error = output::JsonError::new(
2235                        "CgfLoadError",
2236                        &format!("Failed to load CFG for function '{}'", args.function),
2237                        output::E_CFG_ERROR,
2238                    );
2239                    let wrapper = output::JsonResponse::new(error);
2240                    println!("{}", wrapper.to_json());
2241                    std::process::exit(output::EXIT_DATABASE);
2242                } else {
2243                    output::error(&format!(
2244                        "Failed to load CFG for function '{}'",
2245                        args.function
2246                    ));
2247                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
2248                    std::process::exit(output::EXIT_DATABASE);
2249                }
2250            }
2251        };
2252
2253        // Detect natural loops
2254        let natural_loops = detect_natural_loops(&cfg);
2255
2256        // Compute nesting levels for each loop
2257        let loop_infos: Vec<LoopInfo> = natural_loops
2258            .iter()
2259            .map(|loop_| {
2260                let nesting_level = loop_.nesting_level(&natural_loops);
2261                let body_blocks: Vec<usize> = loop_.body.iter().map(|&node| cfg[node].id).collect();
2262                LoopInfo {
2263                    header: cfg[loop_.header].id,
2264                    back_edge_from: cfg[loop_.back_edge.0].id,
2265                    body_size: loop_.size(),
2266                    nesting_level,
2267                    body_blocks,
2268                }
2269            })
2270            .collect();
2271
2272        // Output based on format
2273        match cli.output {
2274            OutputFormat::Human => {
2275                println!("Function: {}", args.function);
2276                println!("Natural Loops: {}", natural_loops.len());
2277                println!();
2278
2279                if natural_loops.is_empty() {
2280                    output::info("No natural loops detected in this function");
2281                } else {
2282                    for (i, loop_info) in loop_infos.iter().enumerate() {
2283                        println!("Loop {}:", i + 1);
2284                        println!("  Header: Block {}", loop_info.header);
2285                        println!("  Back edge from: Block {}", loop_info.back_edge_from);
2286                        println!("  Body size: {} blocks", loop_info.body_size);
2287                        println!("  Nesting level: {}", loop_info.nesting_level);
2288
2289                        if args.verbose {
2290                            println!("  Body blocks: {:?}", loop_info.body_blocks);
2291                        }
2292                        println!();
2293                    }
2294                }
2295            }
2296            OutputFormat::Json | OutputFormat::Pretty => {
2297                let response = LoopsResponse {
2298                    function: args.function.clone(),
2299                    loop_count: natural_loops.len(),
2300                    loops: loop_infos,
2301                };
2302                let wrapper = output::JsonResponse::new(response);
2303                match cli.output {
2304                    OutputFormat::Json => println!("{}", wrapper.to_json()),
2305                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2306                    _ => unreachable!(),
2307                }
2308            }
2309        }
2310
2311        Ok(())
2312    }
2313
2314    pub fn unreachable(args: &UnreachableArgs, cli: &Cli) -> Result<()> {
2315        use crate::analysis::DeadSymbolJson;
2316        use crate::analysis::MagellanBridge;
2317        use crate::cfg::load_cfg_from_db;
2318        use crate::cfg::reachability::find_unreachable;
2319        use crate::storage::MirageDb;
2320        use petgraph::visit::EdgeRef;
2321
2322        // Resolve database path
2323        let db_path = super::resolve_db_path(cli.db.clone())?;
2324
2325        // For --include-uncalled, also open Magellan database
2326        let uncalled_functions: Option<Vec<DeadSymbolJson>> = if args.include_uncalled {
2327            match MagellanBridge::open(&db_path) {
2328                Ok(bridge) => {
2329                    match bridge.dead_symbols("main") {
2330                        Ok(dead) => {
2331                            let json_symbols: Vec<DeadSymbolJson> =
2332                                dead.iter().map(|d| d.into()).collect();
2333                            Some(json_symbols)
2334                        }
2335                        Err(e) => {
2336                            // Log but continue with intra-procedural analysis
2337                            eprintln!("Warning: Failed to detect uncalled functions: {}", e);
2338                            None
2339                        }
2340                    }
2341                }
2342                Err(e) => {
2343                    // Magellan database not available - warn but continue
2344                    eprintln!(
2345                        "Warning: Could not open Magellan database for --include-uncalled: {}",
2346                        e
2347                    );
2348                    eprintln!("Note: --include-uncalled requires a Magellan code graph database");
2349                    None
2350                }
2351            }
2352        } else {
2353            None
2354        };
2355
2356        // Open database (follows status command pattern for error handling)
2357        let db = match MirageDb::open(&db_path) {
2358            Ok(db) => db,
2359            Err(_e) => {
2360                // JSON-aware error handling with remediation
2361                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2362                    let error = output::JsonError::database_not_found(&db_path);
2363                    let wrapper = output::JsonResponse::new(error);
2364                    println!("{}", wrapper.to_json());
2365                    std::process::exit(output::EXIT_DATABASE);
2366                } else {
2367                    output::error(&format!("Failed to open database: {}", db_path));
2368                    output::info("Hint: Run 'magellan watch' to create the database");
2369                    std::process::exit(output::EXIT_DATABASE);
2370                }
2371            }
2372        };
2373
2374        // Struct to hold unreachable results per function
2375        struct FunctionUnreachable {
2376            function_name: String,
2377            function_id: i64,
2378            blocks: Vec<UnreachableBlock>,
2379        }
2380
2381        // Query all functions from the database
2382        // Note: Requires SQLite backend for full table scan
2383        if !db.is_sqlite() {
2384            output::error("The 'unreachable' command currently requires SQLite backend.");
2385            output::info("Use SQLite backend or run with --help for alternatives.");
2386            std::process::exit(output::EXIT_USAGE);
2387        }
2388
2389        // Use prepare and execute to handle multiple rows properly
2390        let mut function_rows: Vec<(String, i64)> = Vec::new();
2391        // Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
2392        let mut stmt = match db.conn()?.prepare(
2393            "SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
2394            Ok(stmt) => stmt,
2395            Err(e) => {
2396                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2397                    let error = output::JsonError::new(
2398                        "QueryError",
2399                        &format!("Failed to query functions: {}", e),
2400                        output::E_DATABASE_NOT_FOUND,
2401                    );
2402                    let wrapper = output::JsonResponse::new(error);
2403                    println!("{}", wrapper.to_json());
2404                    std::process::exit(output::EXIT_DATABASE);
2405                } else {
2406                    output::error(&format!("Failed to query functions: {}", e));
2407                    std::process::exit(output::EXIT_DATABASE);
2408                }
2409            }
2410        };
2411
2412        let rows_result = stmt.query_map([], |row| {
2413            Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
2414        });
2415
2416        match rows_result {
2417            Ok(rows) => {
2418                for row in rows {
2419                    match row {
2420                        Ok((name, id)) => function_rows.push((name, id)),
2421                        Err(e) => {
2422                            if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2423                                let error = output::JsonError::new(
2424                                    "QueryError",
2425                                    &format!("Failed to read function row: {}", e),
2426                                    output::E_DATABASE_NOT_FOUND,
2427                                );
2428                                let wrapper = output::JsonResponse::new(error);
2429                                println!("{}", wrapper.to_json());
2430                                std::process::exit(output::EXIT_DATABASE);
2431                            } else {
2432                                output::error(&format!("Failed to read function row: {}", e));
2433                                std::process::exit(output::EXIT_DATABASE);
2434                            }
2435                        }
2436                    }
2437                }
2438            }
2439            Err(e) => {
2440                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2441                    let error = output::JsonError::new(
2442                        "QueryError",
2443                        &format!("Failed to execute query: {}", e),
2444                        output::E_DATABASE_NOT_FOUND,
2445                    );
2446                    let wrapper = output::JsonResponse::new(error);
2447                    println!("{}", wrapper.to_json());
2448                    std::process::exit(output::EXIT_DATABASE);
2449                } else {
2450                    output::error(&format!("Failed to execute query: {}", e));
2451                    std::process::exit(output::EXIT_DATABASE);
2452                }
2453            }
2454        }
2455
2456        // Load CFG for each function and find unreachable blocks
2457        let mut all_results = Vec::new();
2458        for (function_name, function_id) in function_rows {
2459            match load_cfg_from_db(&db, function_id) {
2460                Ok(cfg) => {
2461                    let unreachable_indices = find_unreachable(&cfg);
2462                    if !unreachable_indices.is_empty() {
2463                        let blocks: Vec<UnreachableBlock> = unreachable_indices
2464                            .iter()
2465                            .map(|&idx| {
2466                                let block = &cfg[idx];
2467                                let kind_str = format!("{:?}", block.kind);
2468                                let terminator_str = format!("{:?}", block.terminator);
2469
2470                                let incoming_edges = if args.show_branches {
2471                                    cfg.edge_references()
2472                                        .filter(|edge| edge.target() == idx)
2473                                        .filter_map(|edge| {
2474                                            let source_block = &cfg[edge.source()];
2475                                            cfg.edge_weight(edge.id()).map(|edge_type| {
2476                                                IncomingEdge {
2477                                                    from_block: source_block.id,
2478                                                    edge_type: format!("{:?}", edge_type),
2479                                                }
2480                                            })
2481                                        })
2482                                        .collect()
2483                                } else {
2484                                    vec![]
2485                                };
2486
2487                                UnreachableBlock {
2488                                    block_id: block.id,
2489                                    kind: kind_str,
2490                                    statements: block.statements.clone(),
2491                                    terminator: terminator_str,
2492                                    incoming_edges,
2493                                }
2494                            })
2495                            .collect();
2496
2497                        all_results.push(FunctionUnreachable {
2498                            function_name,
2499                            function_id,
2500                            blocks,
2501                        });
2502                    }
2503                }
2504                Err(_) => {
2505                    // Skip functions that fail to load
2506                    continue;
2507                }
2508            }
2509        }
2510
2511        // Calculate totals
2512        let total_functions = all_results.len();
2513        let functions_with_unreachable =
2514            all_results.iter().filter(|r| !r.blocks.is_empty()).count();
2515        let total_blocks: usize = all_results.iter().map(|r| r.blocks.len()).sum();
2516
2517        // Format output based on cli.output
2518        match cli.output {
2519            OutputFormat::Human => {
2520                // Show uncalled functions first if available
2521                if let Some(ref uncalled) = uncalled_functions {
2522                    println!("Uncalled Functions ({}):", uncalled.len());
2523                    for dead in uncalled {
2524                        let name = dead.fqn.as_deref().unwrap_or("?");
2525                        println!("  - {} ({})", name, dead.kind);
2526                        println!("    File: {}", dead.file_path);
2527                        println!("    Reason: {}", dead.reason);
2528                    }
2529                    println!();
2530                }
2531
2532                // Show unreachable blocks
2533                if total_blocks == 0 {
2534                    if uncalled_functions.is_none()
2535                        || uncalled_functions
2536                            .as_ref()
2537                            .map(|v| v.is_empty())
2538                            .unwrap_or(false)
2539                    {
2540                        output::info("No unreachable code found");
2541                    }
2542                    return Ok(());
2543                }
2544
2545                println!("Unreachable Code Blocks:");
2546                println!("  Total blocks: {}", total_blocks);
2547                println!(
2548                    "  Functions with unreachable: {}/{}",
2549                    functions_with_unreachable, total_functions
2550                );
2551                println!();
2552
2553                for result in &all_results {
2554                    if result.blocks.is_empty() {
2555                        continue;
2556                    }
2557
2558                    println!("Function: {}", result.function_name);
2559
2560                    for block in &result.blocks {
2561                        println!("  Block {} ({})", block.block_id, block.kind);
2562                        if !block.statements.is_empty() {
2563                            for stmt in &block.statements {
2564                                println!("    - {}", stmt);
2565                            }
2566                        }
2567                        println!("    Terminator: {}", block.terminator);
2568                        println!();
2569                    }
2570
2571                    if args.show_branches {
2572                        println!("  Incoming Edges:");
2573                        for block in &result.blocks {
2574                            if block.incoming_edges.is_empty() {
2575                                println!(
2576                                    "    Block {} has no incoming edges (entry or isolated)",
2577                                    block.block_id
2578                                );
2579                            } else {
2580                                println!("    Block {} incoming edges:", block.block_id);
2581                                for edge in &block.incoming_edges {
2582                                    println!(
2583                                        "      from block {} ({})",
2584                                        edge.from_block, edge.edge_type
2585                                    );
2586                                }
2587                            }
2588                        }
2589                        println!();
2590                    }
2591                }
2592            }
2593            OutputFormat::Json | OutputFormat::Pretty => {
2594                // For multi-function mode, flatten blocks across all functions
2595                let all_blocks: Vec<UnreachableBlock> =
2596                    all_results.iter().flat_map(|r| r.blocks.clone()).collect();
2597
2598                let response = UnreachableResponse {
2599                    function: "all".to_string(),
2600                    total_functions,
2601                    functions_with_unreachable,
2602                    unreachable_count: total_blocks,
2603                    blocks: all_blocks,
2604                    uncalled_functions: uncalled_functions,
2605                };
2606                let wrapper = output::JsonResponse::new(response);
2607
2608                match cli.output {
2609                    OutputFormat::Json => println!("{}", wrapper.to_json()),
2610                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2611                    _ => {}
2612                }
2613            }
2614        }
2615
2616        Ok(())
2617    }
2618
2619    pub fn verify(args: &VerifyArgs, cli: &Cli) -> Result<()> {
2620        use crate::cfg::{enumerate_paths, load_cfg_from_db, PathLimits};
2621        use crate::storage::MirageDb;
2622        use rusqlite::OptionalExtension;
2623
2624        // Resolve database path
2625        let db_path = super::resolve_db_path(cli.db.clone())?;
2626
2627        // Open database (follows status command pattern for error handling)
2628        let db = match MirageDb::open(&db_path) {
2629            Ok(db) => db,
2630            Err(_e) => {
2631                // JSON-aware error handling with remediation
2632                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2633                    let error = output::JsonError::database_not_found(&db_path);
2634                    let wrapper = output::JsonResponse::new(error);
2635                    println!("{}", wrapper.to_json());
2636                    std::process::exit(output::EXIT_DATABASE);
2637                } else {
2638                    output::error(&format!("Failed to open database: {}", db_path));
2639                    output::info("Hint: Run 'magellan watch' to create the database");
2640                    std::process::exit(output::EXIT_DATABASE);
2641                }
2642            }
2643        };
2644
2645        let path_id = &args.path_id;
2646
2647        // Path verification requires SQLite backend (path caching)
2648        if !db.is_sqlite() {
2649            let msg = "Path verification requires SQLite backend with path caching.";
2650            if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2651                let error =
2652                    output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
2653                let wrapper = output::JsonResponse::new(error);
2654                println!("{}", wrapper.to_json());
2655                std::process::exit(output::EXIT_USAGE);
2656            } else {
2657                output::error(msg);
2658                output::info("retired binary backend backend does not support path caching.");
2659                std::process::exit(output::EXIT_USAGE);
2660            }
2661        }
2662
2663        // Check if path exists in cache by querying cfg_paths table
2664        let cached_path_info: Option<(String, i64, String)> = db
2665            .conn()?
2666            .query_row(
2667                "SELECT path_id, function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
2668                rusqlite::params![path_id],
2669                |row| {
2670                    Ok((
2671                        row.get::<_, String>(0)?,
2672                        row.get::<_, i64>(1)?,
2673                        row.get::<_, String>(2)?,
2674                    ))
2675                },
2676            )
2677            .optional()
2678            .unwrap_or(None);
2679
2680        let (found_in_cache, function_id, _path_kind) = match cached_path_info {
2681            Some((_id, fid, kind)) => (true, fid, kind),
2682            None => {
2683                // Path not found in cache
2684                let result = VerifyResult {
2685                    path_id: path_id.clone(),
2686                    valid: false,
2687                    found_in_cache: false,
2688                    function_id: None,
2689                    reason: "Path not found in cache".to_string(),
2690                    current_paths: 0,
2691                };
2692
2693                match cli.output {
2694                    OutputFormat::Human => {
2695                        println!("Path ID {}: not found in cache", path_id);
2696                        println!("  The path may have been invalidated or never existed.");
2697                    }
2698                    OutputFormat::Json | OutputFormat::Pretty => {
2699                        let wrapper = output::JsonResponse::new(result);
2700                        match cli.output {
2701                            OutputFormat::Json => println!("{}", wrapper.to_json()),
2702                            OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2703                            _ => unreachable!(),
2704                        }
2705                    }
2706                }
2707                return Ok(());
2708            }
2709        };
2710
2711        // Path exists in cache - verify it still exists in current enumeration
2712        // Load CFG from database for this function
2713        let cfg = match load_cfg_from_db(&db, function_id) {
2714            Ok(cfg) => cfg,
2715            Err(_e) => {
2716                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2717                    let error = output::JsonError::new(
2718                        "CgfLoadError",
2719                        &format!("Failed to load CFG for function_id {}", function_id),
2720                        output::E_CFG_ERROR,
2721                    );
2722                    let wrapper = output::JsonResponse::new(error);
2723                    println!("{}", wrapper.to_json());
2724                    std::process::exit(output::EXIT_DATABASE);
2725                } else {
2726                    output::error(&format!(
2727                        "Failed to load CFG for function_id {}",
2728                        function_id
2729                    ));
2730                    output::info(
2731                        "The function data may be corrupted. Try re-running 'magellan watch'",
2732                    );
2733                    std::process::exit(output::EXIT_DATABASE);
2734                }
2735            }
2736        };
2737
2738        // Re-enumerate paths to check if the path still exists
2739        let limits = PathLimits::default();
2740        let current_paths = enumerate_paths(&cfg, &limits);
2741        let current_path_count = current_paths.len();
2742
2743        // Check if any enumerated path has the same path_id
2744        let path_still_valid = current_paths.iter().any(|p| &p.path_id == path_id);
2745
2746        let reason = if path_still_valid {
2747            "Path found in current enumeration".to_string()
2748        } else {
2749            "Path no longer exists in current enumeration (code may have changed)".to_string()
2750        };
2751
2752        let result = VerifyResult {
2753            path_id: path_id.clone(),
2754            valid: path_still_valid,
2755            found_in_cache,
2756            function_id: Some(function_id),
2757            reason,
2758            current_paths: current_path_count,
2759        };
2760
2761        match cli.output {
2762            OutputFormat::Human => {
2763                println!(
2764                    "Path ID {}: {}",
2765                    path_id,
2766                    if result.valid { "valid" } else { "invalid" }
2767                );
2768                println!(
2769                    "  Found in cache: {}",
2770                    if found_in_cache { "yes" } else { "no" }
2771                );
2772                println!("  Status: {}", result.reason);
2773                println!("  Current total paths: {}", current_path_count);
2774                if !path_still_valid {
2775                    println!();
2776                    output::info("The path may have been invalidated by code changes.");
2777                    output::info("Consider re-running path enumeration to update the cache.");
2778                }
2779            }
2780            OutputFormat::Json | OutputFormat::Pretty => {
2781                let wrapper = output::JsonResponse::new(result);
2782                match cli.output {
2783                    OutputFormat::Json => println!("{}", wrapper.to_json()),
2784                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
2785                    _ => unreachable!(),
2786                }
2787            }
2788        }
2789
2790        Ok(())
2791    }
2792
2793    pub fn blast_zone(args: &BlastZoneArgs, cli: &Cli) -> Result<()> {
2794        use crate::cfg::{find_reachable_from_block, load_cfg_from_db, resolve_function_name};
2795        use crate::storage::{compute_path_impact_from_db, get_function_name_db, MirageDb};
2796        use rusqlite::OptionalExtension;
2797
2798        // Resolve database path
2799        let db_path = super::resolve_db_path(cli.db.clone())?;
2800
2801        // Open database (follows status command pattern for error handling)
2802        let db = match MirageDb::open(&db_path) {
2803            Ok(db) => db,
2804            Err(_e) => {
2805                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2806                    let error = output::JsonError::database_not_found(&db_path);
2807                    let wrapper = output::JsonResponse::new(error);
2808                    println!("{}", wrapper.to_json());
2809                    std::process::exit(output::EXIT_DATABASE);
2810                } else {
2811                    output::error(&format!("Failed to open database: {}", db_path));
2812                    output::info("Hint: Run 'magellan watch' to create the database");
2813                    std::process::exit(output::EXIT_DATABASE);
2814                }
2815            }
2816        };
2817
2818        // Determine query type: path-based or block-based
2819        if let Some(ref path_id) = args.path_id {
2820            // Path-based impact analysis requires SQLite backend (path caching)
2821            if !db.is_sqlite() {
2822                let msg = "Path-based blast-zone requires SQLite backend. Use block-based analysis with --function and --block-id instead.";
2823                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2824                    let error =
2825                        output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
2826                    let wrapper = output::JsonResponse::new(error);
2827                    println!("{}", wrapper.to_json());
2828                    std::process::exit(output::EXIT_USAGE);
2829                } else {
2830                    output::error(msg);
2831                    output::info("retired binary backend backend does not support path caching. Use: mirage blast-zone --function <name> --block-id <id>");
2832                    std::process::exit(output::EXIT_USAGE);
2833                }
2834            }
2835
2836            // Path-based impact analysis
2837            let path_id_trimmed = path_id.trim();
2838
2839            // Validate path_id format (basic BLAKE3 hex check)
2840            if path_id_trimmed.len() < 10 {
2841                let msg = format!("Invalid path_id format: '{}'", path_id_trimmed);
2842                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2843                    let error =
2844                        output::JsonError::new("InvalidInput", &msg, output::E_INVALID_INPUT);
2845                    let wrapper = output::JsonResponse::new(error);
2846                    println!("{}", wrapper.to_json());
2847                    std::process::exit(output::EXIT_USAGE);
2848                } else {
2849                    output::error(&msg);
2850                    output::info("Path ID should be a BLAKE3 hash (64 hex characters)");
2851                    std::process::exit(output::EXIT_USAGE);
2852                }
2853            }
2854
2855            // Get path metadata to find function_id
2856            let (function_id, path_kind): (i64, String) = match db
2857                .conn()?
2858                .query_row(
2859                    "SELECT function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
2860                    rusqlite::params![path_id_trimmed],
2861                    |row| Ok((row.get(0)?, row.get(1)?)),
2862                )
2863                .optional()
2864            {
2865                Ok(Some(data)) => data,
2866                Ok(None) => {
2867                    let msg = format!("Path '{}' not found in cache", path_id_trimmed);
2868                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2869                        let error =
2870                            output::JsonError::new("PathNotFound", &msg, output::E_PATH_NOT_FOUND);
2871                        let wrapper = output::JsonResponse::new(error);
2872                        println!("{}", wrapper.to_json());
2873                        std::process::exit(output::EXIT_FILE_NOT_FOUND);
2874                    } else {
2875                        output::error(&msg);
2876                        output::info("Hint: Run 'mirage paths' to enumerate paths first");
2877                        std::process::exit(output::EXIT_FILE_NOT_FOUND);
2878                    }
2879                }
2880                Err(e) => {
2881                    let msg = format!("Failed to query path: {}", e);
2882                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2883                        let error = output::JsonError::new(
2884                            "DatabaseError",
2885                            &msg,
2886                            output::E_DATABASE_NOT_FOUND,
2887                        );
2888                        let wrapper = output::JsonResponse::new(error);
2889                        println!("{}", wrapper.to_json());
2890                        std::process::exit(output::EXIT_DATABASE);
2891                    } else {
2892                        output::error(&msg);
2893                        std::process::exit(output::EXIT_DATABASE);
2894                    }
2895                }
2896            };
2897
2898            // Filter by path_kind if include_errors is false
2899            if !args.include_errors && path_kind == "error" {
2900                let msg = format!(
2901                    "Path '{}' is an error path (use --include-errors to analyze)",
2902                    path_id_trimmed
2903                );
2904                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2905                    let error =
2906                        output::JsonError::new("ErrorPathExcluded", &msg, output::E_INVALID_INPUT);
2907                    let wrapper = output::JsonResponse::new(error);
2908                    println!("{}", wrapper.to_json());
2909                    std::process::exit(output::EXIT_USAGE);
2910                } else {
2911                    output::error(&msg);
2912                    output::info("Use --include-errors to include error paths in analysis");
2913                    std::process::exit(output::EXIT_USAGE);
2914                }
2915            }
2916
2917            // Load CFG for the function
2918            let cfg = match load_cfg_from_db(&db, function_id) {
2919                Ok(cfg) => cfg,
2920                Err(_e) => {
2921                    let msg = format!("Failed to load CFG for function_id {}", function_id);
2922                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2923                        let error =
2924                            output::JsonError::new("CgfLoadError", &msg, output::E_CFG_ERROR);
2925                        let wrapper = output::JsonResponse::new(error);
2926                        println!("{}", wrapper.to_json());
2927                        std::process::exit(output::EXIT_DATABASE);
2928                    } else {
2929                        output::error(&msg);
2930                        output::info(
2931                            "The function may be corrupted. Try re-running 'magellan watch'",
2932                        );
2933                        std::process::exit(output::EXIT_DATABASE);
2934                    }
2935                }
2936            };
2937
2938            // Get function name for display (backend-agnostic)
2939            let function_name = get_function_name_db(&db, function_id)
2940                .unwrap_or_else(|| format!("<function_{}>", function_id));
2941
2942            // Compute path impact
2943            let max_depth = if args.max_depth == 100 {
2944                None
2945            } else {
2946                Some(args.max_depth)
2947            };
2948            let impact =
2949                match compute_path_impact_from_db(db.conn()?, path_id_trimmed, &cfg, max_depth) {
2950                    Ok(impact) => impact,
2951                    Err(e) => {
2952                        let msg = format!("Failed to compute path impact: {}", e);
2953                        if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
2954                            let error =
2955                                output::JsonError::new("ImpactError", &msg, output::E_CFG_ERROR);
2956                            let wrapper = output::JsonResponse::new(error);
2957                            println!("{}", wrapper.to_json());
2958                            std::process::exit(output::EXIT_ERROR);
2959                        } else {
2960                            output::error(&msg);
2961                            std::process::exit(output::EXIT_ERROR);
2962                        }
2963                    }
2964                };
2965
2966            // Compute call graph impact if requested
2967            let (forward_impact, backward_impact): (
2968                Option<Vec<CallGraphSymbol>>,
2969                Option<Vec<CallGraphSymbol>>,
2970            ) = if args.use_call_graph {
2971                use crate::analysis::MagellanBridge;
2972                match MagellanBridge::open(&db_path) {
2973                    Ok(bridge) => {
2974                        // Use function name as symbol identifier
2975                        let symbol_id = function_name.as_str();
2976                        let forward: Option<Vec<CallGraphSymbol>> = bridge
2977                            .reachable_symbols(symbol_id)
2978                            .map(|symbols| {
2979                                symbols
2980                                    .into_iter()
2981                                    .map(|s| CallGraphSymbol {
2982                                        symbol_id: s.symbol_id,
2983                                        fqn: s.fqn,
2984                                        file_path: s.file_path,
2985                                        kind: s.kind,
2986                                    })
2987                                    .collect()
2988                            })
2989                            .ok();
2990                        let backward: Option<Vec<CallGraphSymbol>> = bridge
2991                            .reverse_reachable_symbols(symbol_id)
2992                            .map(|symbols| {
2993                                symbols
2994                                    .into_iter()
2995                                    .map(|s| CallGraphSymbol {
2996                                        symbol_id: s.symbol_id,
2997                                        fqn: s.fqn,
2998                                        file_path: s.file_path,
2999                                        kind: s.kind,
3000                                    })
3001                                    .collect()
3002                            })
3003                            .ok();
3004                        (forward, backward)
3005                    }
3006                    Err(e) => {
3007                        eprintln!(
3008                            "Warning: Could not open Magellan database for call graph analysis: {}",
3009                            e
3010                        );
3011                        eprintln!("Note: --use-call-graph requires a Magellan code graph database");
3012                        (None, None)
3013                    }
3014                }
3015            } else {
3016                (None, None)
3017            };
3018
3019            // Output
3020            match cli.output {
3021                OutputFormat::Human => {
3022                    println!("Path Impact Analysis");
3023                    println!();
3024                    println!("Path ID: {}", impact.path_id);
3025                    println!("Function: {}", function_name);
3026                    println!("Path kind: {}", path_kind);
3027                    println!("Path length: {} blocks", impact.path_length);
3028                    println!();
3029
3030                    // Show call graph impact if available
3031                    if let Some(ref forward) = forward_impact {
3032                        println!("Inter-Procedural Impact (Call Graph):");
3033                        println!("  Forward Impact: {} functions reached", forward.len());
3034                        for sym in forward {
3035                            println!("    - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3036                        }
3037                    }
3038                    if let Some(ref backward) = backward_impact {
3039                        if !backward.is_empty() {
3040                            println!(
3041                                "  Backward Impact: {} functions can reach this",
3042                                backward.len()
3043                            );
3044                            for sym in backward {
3045                                println!("    - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3046                            }
3047                        }
3048                    }
3049                    println!();
3050
3051                    println!("Intra-Procedural Impact (CFG):");
3052                    println!("  Unique blocks affected: {}", impact.impact_count);
3053                    if impact.impact_count > 0 {
3054                        println!("  Affected blocks: {:?}", impact.unique_blocks_affected);
3055                    } else {
3056                        println!("  Affected blocks: (none - path has no downstream impact)");
3057                    }
3058                    if let Some(depth) = max_depth {
3059                        println!("  Max depth: {}", depth);
3060                    } else {
3061                        println!("  Max depth: unlimited");
3062                    }
3063                }
3064                OutputFormat::Json | OutputFormat::Pretty => {
3065                    let response = PathImpactResponse {
3066                        path_id: impact.path_id.clone(),
3067                        path_length: impact.path_length,
3068                        unique_blocks_affected: impact.unique_blocks_affected,
3069                        impact_count: impact.impact_count,
3070                        forward_impact: forward_impact.clone(),
3071                        backward_impact: backward_impact.clone(),
3072                    };
3073                    let wrapper = output::JsonResponse::new(response);
3074                    match cli.output {
3075                        OutputFormat::Json => println!("{}", wrapper.to_json()),
3076                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3077                        _ => unreachable!(),
3078                    }
3079                }
3080            }
3081        } else {
3082            // Block-based impact analysis
3083            // Get function from args
3084            let function_ref = args
3085                .function
3086                .as_ref()
3087                .expect("--function is required for block-based analysis");
3088
3089            // Resolve function name/ID to function_id
3090            let function_id = match resolve_function_name(&db, function_ref) {
3091                Ok(id) => id,
3092                Err(_e) => {
3093                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3094                        let error = output::JsonError::function_not_found(function_ref);
3095                        let wrapper = output::JsonResponse::new(error);
3096                        println!("{}", wrapper.to_json());
3097                        std::process::exit(output::EXIT_DATABASE);
3098                    } else {
3099                        output::error(&format!(
3100                            "Function '{}' not found in database",
3101                            function_ref
3102                        ));
3103                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
3104                        std::process::exit(output::EXIT_DATABASE);
3105                    }
3106                }
3107            };
3108
3109            // Get function name for display (backend-agnostic)
3110            let function_name = get_function_name_db(&db, function_id)
3111                .unwrap_or_else(|| format!("<function_{}>", function_id));
3112
3113            // Load CFG from database
3114            let cfg = match load_cfg_from_db(&db, function_id) {
3115                Ok(cfg) => cfg,
3116                Err(_e) => {
3117                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3118                        let error = output::JsonError::new(
3119                            "CgfLoadError",
3120                            &format!("Failed to load CFG for function '{}'", function_ref),
3121                            output::E_CFG_ERROR,
3122                        );
3123                        let wrapper = output::JsonResponse::new(error);
3124                        println!("{}", wrapper.to_json());
3125                        std::process::exit(output::EXIT_DATABASE);
3126                    } else {
3127                        output::error(&format!(
3128                            "Failed to load CFG for function '{}'",
3129                            function_ref
3130                        ));
3131                        output::info(
3132                            "The function may be corrupted. Try re-running 'magellan watch'",
3133                        );
3134                        std::process::exit(output::EXIT_DATABASE);
3135                    }
3136                }
3137            };
3138
3139            // Determine block ID (default to entry block 0)
3140            let block_id = args.block_id.unwrap_or(0);
3141
3142            // Validate block_id exists in CFG
3143            let block_exists = cfg.node_indices().any(|n| cfg[n].id == block_id);
3144            if !block_exists {
3145                let valid_blocks: Vec<usize> = cfg.node_indices().map(|n| cfg[n].id).collect();
3146                let msg = format!(
3147                    "Block {} not found in function '{}'. Valid blocks: {:?}",
3148                    block_id, function_ref, valid_blocks
3149                );
3150                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3151                    let error =
3152                        output::JsonError::new("BlockNotFound", &msg, output::E_BLOCK_NOT_FOUND);
3153                    let wrapper = output::JsonResponse::new(error);
3154                    println!("{}", wrapper.to_json());
3155                    std::process::exit(output::EXIT_VALIDATION);
3156                } else {
3157                    output::error(&msg);
3158                    std::process::exit(output::EXIT_VALIDATION);
3159                }
3160            }
3161
3162            // Compute block impact
3163            let max_depth = if args.max_depth == 100 {
3164                None
3165            } else {
3166                Some(args.max_depth)
3167            };
3168            let impact = find_reachable_from_block(&cfg, block_id, max_depth);
3169
3170            // Compute call graph impact if requested
3171            let (forward_impact, backward_impact): (
3172                Option<Vec<CallGraphSymbol>>,
3173                Option<Vec<CallGraphSymbol>>,
3174            ) = if args.use_call_graph {
3175                use crate::analysis::MagellanBridge;
3176                match MagellanBridge::open(&db_path) {
3177                    Ok(bridge) => {
3178                        // Use function name as symbol identifier
3179                        let symbol_id = function_name.as_str();
3180                        let forward: Option<Vec<CallGraphSymbol>> = bridge
3181                            .reachable_symbols(symbol_id)
3182                            .map(|symbols| {
3183                                symbols
3184                                    .into_iter()
3185                                    .map(|s| CallGraphSymbol {
3186                                        symbol_id: s.symbol_id,
3187                                        fqn: s.fqn,
3188                                        file_path: s.file_path,
3189                                        kind: s.kind,
3190                                    })
3191                                    .collect()
3192                            })
3193                            .ok();
3194                        let backward: Option<Vec<CallGraphSymbol>> = bridge
3195                            .reverse_reachable_symbols(symbol_id)
3196                            .map(|symbols| {
3197                                symbols
3198                                    .into_iter()
3199                                    .map(|s| CallGraphSymbol {
3200                                        symbol_id: s.symbol_id,
3201                                        fqn: s.fqn,
3202                                        file_path: s.file_path,
3203                                        kind: s.kind,
3204                                    })
3205                                    .collect()
3206                            })
3207                            .ok();
3208                        (forward, backward)
3209                    }
3210                    Err(e) => {
3211                        eprintln!(
3212                            "Warning: Could not open Magellan database for call graph analysis: {}",
3213                            e
3214                        );
3215                        eprintln!("Note: --use-call-graph requires a Magellan code graph database");
3216                        (None, None)
3217                    }
3218                }
3219            } else {
3220                (None, None)
3221            };
3222
3223            // Output
3224            match cli.output {
3225                OutputFormat::Human => {
3226                    println!("Block Impact Analysis (Blast Zone)");
3227                    println!();
3228                    println!("Function: {}", function_name);
3229                    println!("Source block: {}", impact.source_block_id);
3230                    println!();
3231
3232                    // Show call graph impact if available
3233                    if let Some(ref forward) = forward_impact {
3234                        println!("Inter-Procedural Impact (Call Graph):");
3235                        println!("  Forward Impact: {} functions reached", forward.len());
3236                        for sym in forward {
3237                            println!("    - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3238                        }
3239                    }
3240                    if let Some(ref backward) = backward_impact {
3241                        if !backward.is_empty() {
3242                            println!(
3243                                "  Backward Impact: {} functions can reach this",
3244                                backward.len()
3245                            );
3246                            for sym in backward {
3247                                println!("    - {}", sym.fqn.as_deref().unwrap_or(&sym.file_path));
3248                            }
3249                        }
3250                    }
3251                    println!();
3252
3253                    println!("Intra-Procedural Impact (CFG):");
3254                    println!("  Reachable blocks: {}", impact.reachable_count);
3255                    if impact.reachable_count > 0 {
3256                        println!("  Affected blocks: {:?}", impact.reachable_blocks);
3257                    } else {
3258                        println!("  Affected blocks: (none - block has no downstream impact)");
3259                    }
3260                    println!("  Max depth reached: {}", impact.max_depth_reached);
3261                    println!(
3262                        "  Contains cycles: {}",
3263                        if impact.has_cycles {
3264                            "yes (loop detected)"
3265                        } else {
3266                            "no"
3267                        }
3268                    );
3269                    if let Some(depth) = max_depth {
3270                        println!("  Depth limit: {}", depth);
3271                    } else {
3272                        println!("  Depth limit: unlimited");
3273                    }
3274                }
3275                OutputFormat::Json | OutputFormat::Pretty => {
3276                    let response = BlockImpactResponse {
3277                        function: function_name,
3278                        block_id: impact.source_block_id,
3279                        reachable_blocks: impact.reachable_blocks,
3280                        reachable_count: impact.reachable_count,
3281                        max_depth: impact.max_depth_reached,
3282                        has_cycles: impact.has_cycles,
3283                        forward_impact: forward_impact.clone(),
3284                        backward_impact: backward_impact.clone(),
3285                    };
3286                    let wrapper = output::JsonResponse::new(response);
3287                    match cli.output {
3288                        OutputFormat::Json => println!("{}", wrapper.to_json()),
3289                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3290                        _ => unreachable!(),
3291                    }
3292                }
3293            }
3294        }
3295
3296        Ok(())
3297    }
3298
3299    pub fn cycles(args: &CyclesArgs, cli: &Cli) -> Result<()> {
3300        use crate::analysis::{CycleInfo, EnhancedCycles, LoopInfo, MagellanBridge};
3301        use crate::cfg::detect_natural_loops;
3302        use crate::cfg::load_cfg_from_db;
3303        use crate::storage::MirageDb;
3304
3305        // Resolve database path
3306        let db_path = super::resolve_db_path(cli.db.clone())?;
3307
3308        // Default: show both types if no flag specified
3309        let show_call_graph = args.call_graph
3310            || args.both
3311            || (!args.call_graph && !args.function_loops && !args.both);
3312        let show_function_loops = args.function_loops
3313            || args.both
3314            || (!args.call_graph && !args.function_loops && !args.both);
3315
3316        // Detect call graph cycles if requested
3317        let mut call_graph_cycles: Vec<CycleInfo> = if show_call_graph {
3318            match MagellanBridge::open(&db_path) {
3319                Ok(bridge) => match bridge.detect_cycles() {
3320                    Ok(report) => report.cycles.iter().map(|c| c.into()).collect(),
3321                    Err(e) => {
3322                        eprintln!("Warning: Failed to detect call graph cycles: {}", e);
3323                        vec![]
3324                    }
3325                },
3326                Err(e) => {
3327                    eprintln!(
3328                        "Warning: Could not open Magellan database for call graph cycles: {}",
3329                        e
3330                    );
3331                    eprintln!("Note: Call graph cycles require a Magellan code graph database");
3332                    vec![]
3333                }
3334            }
3335        } else {
3336            vec![]
3337        };
3338
3339        // Filter call graph cycles by type
3340        call_graph_cycles.retain(|c| match args.cycle_type {
3341            CycleTypeArg::All => true,
3342            CycleTypeArg::InterFunction => c.cycle_type == "MutualRecursion",
3343            CycleTypeArg::SelfLoop => c.cycle_type == "SelfLoop",
3344        });
3345
3346        // Detect function loops if requested
3347        let mut function_loops_map: std::collections::HashMap<String, Vec<LoopInfo>> =
3348            std::collections::HashMap::new();
3349
3350        if show_function_loops {
3351            // Open Mirage database
3352            let db = match MirageDb::open(&db_path) {
3353                Ok(db) => db,
3354                Err(_e) => {
3355                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3356                        let error = output::JsonError::database_not_found(&db_path);
3357                        let wrapper = output::JsonResponse::new(error);
3358                        println!("{}", wrapper.to_json());
3359                        std::process::exit(output::EXIT_DATABASE);
3360                    } else {
3361                        output::error(&format!("Failed to open database: {}", db_path));
3362                        output::info("Hint: Run 'magellan watch' to create the database");
3363                        std::process::exit(output::EXIT_DATABASE);
3364                    }
3365                }
3366            };
3367
3368            // Query all functions from the database
3369            // Note: Requires SQLite backend for SQL queries
3370            if !db.is_sqlite() {
3371                output::error(
3372                    "The 'cycles' command with --function-loops requires SQLite backend.",
3373                );
3374                output::info(
3375                    "retired binary backend backend is not yet supported for this feature.",
3376                );
3377                std::process::exit(output::EXIT_USAGE);
3378            }
3379
3380            // Magellan v7 stores functions as kind='Symbol' with data.kind='Function'
3381            let mut stmt = match db.conn()?.prepare(
3382                "SELECT name, id FROM graph_entities WHERE kind = 'Symbol' AND json_extract(data, '$.kind') = 'Function'") {
3383                Ok(stmt) => stmt,
3384                Err(e) => {
3385                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3386                        let error = output::JsonError::new(
3387                            "QueryError",
3388                            &format!("Failed to query functions: {}", e),
3389                            output::E_DATABASE_NOT_FOUND,
3390                        );
3391                        let wrapper = output::JsonResponse::new(error);
3392                        println!("{}", wrapper.to_json());
3393                        std::process::exit(output::EXIT_DATABASE);
3394                    } else {
3395                        output::error(&format!("Failed to query functions: {}", e));
3396                        std::process::exit(output::EXIT_DATABASE);
3397                    }
3398                }
3399            };
3400
3401            let rows_result = stmt.query_map([], |row| {
3402                Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
3403            });
3404
3405            match rows_result {
3406                Ok(rows) => {
3407                    for row in rows {
3408                        if let Ok((function_name, function_id)) = row {
3409                            // Load CFG for this function
3410                            if let Ok(cfg) = load_cfg_from_db(&db, function_id) {
3411                                // Detect natural loops
3412                                let natural_loops = detect_natural_loops(&cfg);
3413
3414                                if !natural_loops.is_empty() {
3415                                    let loop_infos: Vec<LoopInfo> = natural_loops
3416                                        .iter()
3417                                        .map(|loop_| {
3418                                            let nesting_level = loop_.nesting_level(&natural_loops);
3419                                            let body_blocks: Vec<usize> = loop_
3420                                                .body
3421                                                .iter()
3422                                                .map(|&node| cfg[node].id)
3423                                                .collect();
3424                                            LoopInfo {
3425                                                header: cfg[loop_.header].id,
3426                                                back_edge_from: cfg[loop_.back_edge.0].id,
3427                                                body_size: loop_.size(),
3428                                                nesting_level,
3429                                                body_blocks,
3430                                            }
3431                                        })
3432                                        .collect();
3433
3434                                    function_loops_map.insert(function_name, loop_infos);
3435                                }
3436                            }
3437                        }
3438                    }
3439                }
3440                Err(e) => {
3441                    eprintln!("Warning: Failed to execute query: {}", e);
3442                }
3443            }
3444        }
3445
3446        // Combine results
3447        let total_cycles =
3448            call_graph_cycles.len() + function_loops_map.values().map(|v| v.len()).sum::<usize>();
3449
3450        let enhanced_cycles = EnhancedCycles {
3451            call_graph_cycles,
3452            function_loops: function_loops_map.clone(),
3453            total_cycles,
3454        };
3455
3456        // Output based on format
3457        match cli.output {
3458            OutputFormat::Human => {
3459                println!("Cycle Detection Report");
3460                println!();
3461
3462                if show_call_graph {
3463                    println!(
3464                        "Call Graph Cycles (Inter-procedural): {}",
3465                        enhanced_cycles.call_graph_cycles.len()
3466                    );
3467                    if enhanced_cycles.call_graph_cycles.is_empty() {
3468                        println!("  No call graph cycles detected");
3469                    } else {
3470                        for (i, cycle) in enhanced_cycles.call_graph_cycles.iter().enumerate() {
3471                            println!("  Cycle {}:", i + 1);
3472                            println!("    Type: {}", cycle.cycle_type);
3473                            println!("    Size: {} symbols", cycle.size);
3474                            if args.verbose {
3475                                println!("    Members:");
3476                                for member in &cycle.members {
3477                                    println!("      - {}", member);
3478                                }
3479                            }
3480                        }
3481                    }
3482                    println!();
3483                }
3484
3485                if show_function_loops {
3486                    println!(
3487                        "Function Loops (Intra-procedural): {} functions with loops",
3488                        enhanced_cycles.function_loops.len()
3489                    );
3490                    if enhanced_cycles.function_loops.is_empty() {
3491                        println!("  No natural loops detected in any function");
3492                    } else {
3493                        for (function_name, loops) in &enhanced_cycles.function_loops {
3494                            println!("  Function: {} ({} loops)", function_name, loops.len());
3495                            if args.verbose {
3496                                for (i, loop_info) in loops.iter().enumerate() {
3497                                    println!("    Loop {}:", i + 1);
3498                                    println!("      Header: Block {}", loop_info.header);
3499                                    println!(
3500                                        "      Back edge from: Block {}",
3501                                        loop_info.back_edge_from
3502                                    );
3503                                    println!("      Body size: {} blocks", loop_info.body_size);
3504                                    println!("      Nesting level: {}", loop_info.nesting_level);
3505                                    println!("      Body blocks: {:?}", loop_info.body_blocks);
3506                                }
3507                            }
3508                        }
3509                    }
3510                    println!();
3511                }
3512
3513                println!("Total cycles: {}", total_cycles);
3514            }
3515            OutputFormat::Json | OutputFormat::Pretty => {
3516                let wrapper = output::JsonResponse::new(enhanced_cycles);
3517                match cli.output {
3518                    OutputFormat::Json => println!("{}", wrapper.to_json()),
3519                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3520                    _ => unreachable!(),
3521                }
3522            }
3523        }
3524
3525        Ok(())
3526    }
3527
3528    pub fn slice(args: &SliceArgs, cli: &Cli) -> Result<()> {
3529        use crate::analysis::{MagellanBridge, SliceWrapper};
3530
3531        // Resolve database path
3532        let db_path = super::resolve_db_path(cli.db.clone())?;
3533
3534        // Open Magellan database
3535        let bridge = match MagellanBridge::open(&db_path) {
3536            Ok(bridge) => bridge,
3537            Err(e) => {
3538                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3539                    let error = output::JsonError::new(
3540                        "DatabaseError",
3541                        &format!("Failed to open Magellan database: {}", e),
3542                        output::E_DATABASE_NOT_FOUND,
3543                    );
3544                    let wrapper = output::JsonResponse::new(error);
3545                    println!("{}", wrapper.to_json());
3546                    std::process::exit(output::EXIT_DATABASE);
3547                } else {
3548                    output::error(&format!("Failed to open Magellan database: {}", e));
3549                    output::info("Note: Program slicing requires a Magellan code graph database");
3550                    std::process::exit(output::EXIT_DATABASE);
3551                }
3552            }
3553        };
3554
3555        // Perform the slice based on direction
3556        let slice_result: SliceWrapper = match args.direction {
3557            SliceDirectionArg::Backward => bridge.backward_slice(&args.symbol)?,
3558            SliceDirectionArg::Forward => bridge.forward_slice(&args.symbol)?,
3559        };
3560
3561        // Output based on format
3562        match cli.output {
3563            OutputFormat::Human => {
3564                println!("Program Slice: {}", slice_result.direction);
3565                println!();
3566
3567                // Target symbol
3568                println!("Target:");
3569                println!(
3570                    "  Symbol: {}",
3571                    slice_result.target.fqn.as_deref().unwrap_or(&args.symbol)
3572                );
3573                println!("  Kind: {}", slice_result.target.kind);
3574                println!("  File: {}", slice_result.target.file_path);
3575                println!();
3576
3577                // Statistics
3578                println!("Statistics:");
3579                println!("  Total symbols in slice: {}", slice_result.symbol_count);
3580                println!(
3581                    "  Data dependencies: {}",
3582                    slice_result.statistics.data_dependencies
3583                );
3584                println!(
3585                    "  Control dependencies: {}",
3586                    slice_result.statistics.control_dependencies
3587                );
3588                println!();
3589
3590                // Included symbols (verbose only)
3591                if args.verbose {
3592                    println!(
3593                        "Included symbols ({}):",
3594                        slice_result.included_symbols.len()
3595                    );
3596                    for (i, symbol) in slice_result.included_symbols.iter().enumerate() {
3597                        println!(
3598                            "  {}. {}",
3599                            i + 1,
3600                            symbol.fqn.as_deref().unwrap_or("<unknown>")
3601                        );
3602                        println!("     Kind: {}, File: {}", symbol.kind, symbol.file_path);
3603                    }
3604                } else {
3605                    println!("Use --verbose to see all included symbols");
3606                }
3607            }
3608            OutputFormat::Json | OutputFormat::Pretty => {
3609                let wrapper = output::JsonResponse::new(slice_result);
3610                match cli.output {
3611                    OutputFormat::Json => println!("{}", wrapper.to_json()),
3612                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
3613                    _ => unreachable!(),
3614                }
3615            }
3616        }
3617
3618        Ok(())
3619    }
3620
3621    pub fn hotspots(args: &HotspotsArgs, cli: &Cli) -> Result<()> {
3622        use crate::analysis::MagellanBridge;
3623        #[cfg(feature = "sqlite")]
3624        use crate::cfg::{
3625            enumerate_paths_with_context, load_cfg_from_db_with_conn, EnumerationContext,
3626            PathLimits,
3627        };
3628        #[cfg(feature = "sqlite")]
3629        use crate::storage::MirageDb;
3630        use std::collections::HashMap;
3631
3632        let db_path = super::resolve_db_path(cli.db.clone())?;
3633
3634        // Open Mirage database for intra-procedural analysis
3635        #[cfg(feature = "sqlite")]
3636        let mut db = match MirageDb::open(&db_path) {
3637            Ok(db) => db,
3638            Err(e) => {
3639                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3640                    let error = output::JsonError::new(
3641                        "DatabaseError",
3642                        &format!("Failed to open database: {}", e),
3643                        output::E_DATABASE_NOT_FOUND,
3644                    );
3645                    let wrapper = output::JsonResponse::new(error);
3646                    println!("{}", wrapper.to_json());
3647                    std::process::exit(output::EXIT_DATABASE);
3648                } else {
3649                    output::error(&format!("Failed to open database: {}", e));
3650                    output::info("Hint: Run 'magellan watch' to create the database");
3651                    std::process::exit(output::EXIT_DATABASE);
3652                }
3653            }
3654        };
3655
3656        let mut hotspots: Vec<HotspotEntry> = Vec::new();
3657        let mut function_count = 0;
3658
3659        if args.inter_procedural {
3660            // Inter-procedural: Use Magellan for call graph analysis
3661            match MagellanBridge::open(&db_path) {
3662                Ok(bridge) => {
3663                    // Get path enumeration from entry point
3664                    let path_result = bridge.enumerate_paths(&args.entry, None, 50, args.top * 10);
3665
3666                    if let Ok(paths) = path_result {
3667                        // Count paths through each function
3668                        let mut path_counts: HashMap<String, usize> = HashMap::new();
3669
3670                        for path in &paths.paths {
3671                            for symbol in &path.symbols {
3672                                if let Some(fqn) = &symbol.fqn {
3673                                    *path_counts.entry(fqn.clone()).or_insert(0) += 1;
3674                                }
3675                            }
3676                        }
3677
3678                        // Get condensation for dominance (SCC size indicates coupling)
3679                        let condensed = bridge.condense_call_graph();
3680                        if let Ok(condensed) = condensed {
3681                            let mut scc_sizes: HashMap<String, f64> = HashMap::new();
3682
3683                            for supernode in &condensed.graph.supernodes {
3684                                let size = supernode.members.len() as f64;
3685                                for member in &supernode.members {
3686                                    if let Some(fqn) = &member.fqn {
3687                                        scc_sizes.insert(fqn.clone(), size);
3688                                    }
3689                                }
3690                            }
3691
3692                            // Combine metrics for hotspot scoring
3693                            for (fqn, path_count) in &path_counts {
3694                                if *path_count >= args.min_paths.unwrap_or(1) {
3695                                    let dominance = scc_sizes.get(fqn).copied().unwrap_or(1.0);
3696                                    let risk_score = (*path_count as f64) * 1.0 + dominance * 2.0;
3697
3698                                    hotspots.push(HotspotEntry {
3699                                        function: fqn.clone(),
3700                                        risk_score,
3701                                        path_count: *path_count,
3702                                        dominance_factor: dominance,
3703                                        complexity: 0, // Would need CFG for this
3704                                        file_path: "".to_string(),
3705                                    });
3706                                }
3707                            }
3708
3709                            // Count total functions found in inter-procedural analysis
3710                            function_count = path_counts.len();
3711                        }
3712                    }
3713                }
3714                Err(_) => {
3715                    output::warn(
3716                        "Magellan database not available, using intra-procedural analysis",
3717                    );
3718                }
3719            }
3720        }
3721
3722        // Fallback to intra-procedural if no hotspots found or inter-procedural failed
3723        #[cfg(feature = "sqlite")]
3724        if hotspots.is_empty() && db.is_sqlite() {
3725            // Get all functions from database by joining with graph_entities
3726            let conn = db.conn_mut()?;
3727
3728            let query = "SELECT DISTINCT cb.function_id, ge.name, ge.file_path
3729                        FROM cfg_blocks cb
3730                        JOIN graph_entities ge ON cb.function_id = ge.id";
3731            let mut stmt = conn.prepare(query)?;
3732
3733            let function_rows = stmt.query_map([], |row: &rusqlite::Row| {
3734                Ok((
3735                    row.get::<_, i64>(0)?,
3736                    row.get::<_, String>(1)?,
3737                    row.get::<_, String>(2)?,
3738                ))
3739            })?;
3740
3741            for func_result in function_rows {
3742                if let Ok((func_id, func_name, file_path)) = func_result {
3743                    function_count += 1;
3744
3745                    // Load CFG and enumerate paths
3746                    if let Ok(cfg) = load_cfg_from_db_with_conn(conn, func_id) {
3747                        let ctx = EnumerationContext::new(&cfg);
3748                        let limits = PathLimits::quick_analysis();
3749                        let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3750
3751                        let path_count = paths.len();
3752                        if path_count < args.min_paths.unwrap_or(1) {
3753                            continue;
3754                        }
3755
3756                        // Complexity = block count
3757                        let complexity = cfg.node_count();
3758                        let dominance = 1.0; // Intra-procedural doesn't have call dominance
3759                        let risk_score = path_count as f64 * 0.5 + complexity as f64 * 0.1;
3760
3761                        hotspots.push(HotspotEntry {
3762                            function: func_name.clone(),
3763                            risk_score,
3764                            path_count,
3765                            dominance_factor: dominance,
3766                            complexity,
3767                            file_path,
3768                        });
3769                    }
3770                }
3771            }
3772        }
3773
3774        // Sort by risk score (descending) - use total_cmp for f64 comparison
3775        hotspots.sort_by(|a, b| b.risk_score.total_cmp(&a.risk_score));
3776
3777        // Limit to top N
3778        hotspots.truncate(args.top);
3779
3780        let response = HotspotsResponse {
3781            entry_point: args.entry.clone(),
3782            total_functions: function_count,
3783            hotspots: hotspots.clone(),
3784            mode: if args.inter_procedural {
3785                "inter-procedural"
3786            } else {
3787                "intra-procedural"
3788            }
3789            .to_string(),
3790        };
3791
3792        match cli.output {
3793            OutputFormat::Human => {
3794                output::header(&format!(
3795                    "Hotspots Analysis (entry: {})",
3796                    response.entry_point
3797                ));
3798
3799                // Add helpful hint if 0 functions found with intra-procedural mode
3800                if response.total_functions == 0 && response.mode == "intra-procedural" {
3801                    output::warn("No functions found. This may be because:");
3802                    output::info("  1. The database hasn't been indexed yet");
3803                    output::info("  2. You need to run: magellan watch --db <path>");
3804                    output::info("  3. Try --inter-procedural for call-graph-based analysis");
3805                    println!();
3806                }
3807
3808                output::info(&format!(
3809                    "Found {} hotspots out of {} functions",
3810                    hotspots.len(),
3811                    response.total_functions
3812                ));
3813                println!();
3814
3815                for (i, hotspot) in hotspots.iter().enumerate() {
3816                    println!(
3817                        "{}. {} (risk: {:.1})",
3818                        i + 1,
3819                        hotspot.function,
3820                        hotspot.risk_score
3821                    );
3822                    if args.verbose {
3823                        println!("   Paths: {}", hotspot.path_count);
3824                        println!("   Dominance: {:.1}", hotspot.dominance_factor);
3825                        println!("   Complexity: {}", hotspot.complexity);
3826                    }
3827                }
3828            }
3829            OutputFormat::Json => {
3830                let wrapper = output::JsonResponse::new(response);
3831                println!("{}", wrapper.to_json());
3832            }
3833            OutputFormat::Pretty => {
3834                let wrapper = output::JsonResponse::new(response);
3835                println!("{}", wrapper.to_pretty_json());
3836            }
3837        }
3838
3839        Ok(())
3840    }
3841
3842    pub fn hotpaths(args: &HotpathsArgs, cli: &Cli) -> Result<()> {
3843        use crate::cfg::{
3844            detect_natural_loops, enumerate_paths, find_entry,
3845            hotpaths::{compute_hot_paths, HotpathsOptions},
3846            PathLimits,
3847        };
3848        use crate::storage::MirageDb;
3849
3850        // Resolve database path
3851        let db_path = super::resolve_db_path(cli.db.clone())?;
3852
3853        // Open database (follows status command pattern for error handling)
3854        let db = match MirageDb::open(&db_path) {
3855            Ok(db) => db,
3856            Err(_e) => {
3857                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3858                    let error = output::JsonError::database_not_found(&db_path);
3859                    let wrapper = output::JsonResponse::new(error);
3860                    println!("{}", wrapper.to_json());
3861                    std::process::exit(output::EXIT_DATABASE);
3862                } else {
3863                    output::error(&format!("Failed to open database: {}", db_path));
3864                    output::info("Hint: Run 'magellan watch' to create the database");
3865                    std::process::exit(output::EXIT_DATABASE);
3866                }
3867            }
3868        };
3869
3870        // Resolve function name/ID to function_id
3871        let function_id = match db.resolve_function_name(&args.function) {
3872            Ok(id) => id,
3873            Err(_e) => {
3874                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3875                    let error = output::JsonError::function_not_found(&args.function);
3876                    let wrapper = output::JsonResponse::new(error);
3877                    println!("{}", wrapper.to_json());
3878                    std::process::exit(output::EXIT_DATABASE);
3879                } else {
3880                    output::error(&format!(
3881                        "Function '{}' not found in database",
3882                        args.function
3883                    ));
3884                    output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
3885                    std::process::exit(output::EXIT_DATABASE);
3886                }
3887            }
3888        };
3889
3890        // Load CFG from database
3891        let cfg = match db.load_cfg(function_id) {
3892            Ok(cfg) => cfg,
3893            Err(_e) => {
3894                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3895                    let error = output::JsonError::new(
3896                        "CfgLoadError",
3897                        &format!("Failed to load CFG for function '{}'", args.function),
3898                        output::E_CFG_ERROR,
3899                    );
3900                    let wrapper = output::JsonResponse::new(error);
3901                    println!("{}", wrapper.to_json());
3902                    std::process::exit(output::EXIT_DATABASE);
3903                } else {
3904                    output::error(&format!(
3905                        "Failed to load CFG for function '{}'",
3906                        args.function
3907                    ));
3908                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
3909                    std::process::exit(output::EXIT_DATABASE);
3910                }
3911            }
3912        };
3913
3914        // Find entry block
3915        let entry = match find_entry(&cfg) {
3916            Some(entry) => entry,
3917            None => {
3918                output::error(&format!(
3919                    "No entry block found for function '{}'",
3920                    args.function
3921                ));
3922                std::process::exit(output::EXIT_DATABASE);
3923            }
3924        };
3925
3926        // Detect natural loops
3927        let natural_loops = detect_natural_loops(&cfg);
3928
3929        // Enumerate all paths with default limits
3930        // Note: HotpathsArgs uses 'top' for number of results, not path enumeration limits
3931        let limits = PathLimits::default();
3932        let paths = enumerate_paths(&cfg, &limits);
3933
3934        if paths.is_empty() {
3935            output::info(&format!("No paths found for function '{}'", args.function));
3936            return Ok(());
3937        }
3938
3939        // Compute hot paths
3940        let options = HotpathsOptions {
3941            top_n: args.top,
3942            include_rationale: args.rationale,
3943        };
3944
3945        let mut hot_paths = match compute_hot_paths(&cfg, &paths, entry, &natural_loops, options) {
3946            Ok(hp) => hp,
3947            Err(e) => {
3948                output::error(&format!("Failed to compute hot paths: {}", e));
3949                std::process::exit(output::EXIT_DATABASE);
3950            }
3951        };
3952
3953        // Apply minimum score filter if specified
3954        if let Some(min_score) = args.min_score {
3955            hot_paths.retain(|hp| hp.hotness_score >= min_score);
3956        }
3957
3958        // Output based on format
3959        match cli.output {
3960            OutputFormat::Human => {
3961                print_hotpaths_human(&hot_paths, args.rationale);
3962            }
3963            OutputFormat::Json => {
3964                println!("{}", serde_json::to_string(&hot_paths)?);
3965            }
3966            OutputFormat::Pretty => {
3967                println!("{}", serde_json::to_string_pretty(&hot_paths)?);
3968            }
3969        }
3970
3971        Ok(())
3972    }
3973
3974    pub fn patterns(args: &PatternsArgs, cli: &Cli) -> Result<()> {
3975        use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
3976        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
3977        use crate::storage::MirageDb;
3978
3979        // Resolve database path
3980        let db_path = super::resolve_db_path(cli.db.clone())?;
3981
3982        // Open database (follows status command pattern for error handling)
3983        let db = match MirageDb::open(&db_path) {
3984            Ok(db) => db,
3985            Err(_e) => {
3986                // JSON-aware error handling with remediation
3987                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
3988                    let error = output::JsonError::database_not_found(&db_path);
3989                    let wrapper = output::JsonResponse::new(error);
3990                    println!("{}", wrapper.to_json());
3991                    std::process::exit(output::EXIT_DATABASE);
3992                } else {
3993                    output::error(&format!("Failed to open database: {}", db_path));
3994                    output::info("Hint: Run 'magellan watch' to create the database");
3995                    std::process::exit(output::EXIT_DATABASE);
3996                }
3997            }
3998        };
3999
4000        // Resolve function name/ID to function_id (with optional file filter)
4001        let function_id =
4002            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4003                Ok(id) => id,
4004                Err(_e) => {
4005                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4006                        let error = output::JsonError::function_not_found(&args.function);
4007                        let wrapper = output::JsonResponse::new(error);
4008                        println!("{}", wrapper.to_json());
4009                        std::process::exit(output::EXIT_DATABASE);
4010                    } else {
4011                        output::error(&format!(
4012                            "Function '{}' not found in database",
4013                            args.function
4014                        ));
4015                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4016                        std::process::exit(output::EXIT_DATABASE);
4017                    }
4018                }
4019            };
4020
4021        // Load CFG from database
4022        let cfg = match load_cfg_from_db(&db, function_id) {
4023            Ok(cfg) => cfg,
4024            Err(_e) => {
4025                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4026                    let error = output::JsonError::new(
4027                        "CgfLoadError",
4028                        &format!("Failed to load CFG for function '{}'", args.function),
4029                        output::E_CFG_ERROR,
4030                    );
4031                    let wrapper = output::JsonResponse::new(error);
4032                    println!("{}", wrapper.to_json());
4033                    std::process::exit(output::EXIT_DATABASE);
4034                } else {
4035                    output::error(&format!(
4036                        "Failed to load CFG for function '{}'",
4037                        args.function
4038                    ));
4039                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
4040                    std::process::exit(output::EXIT_DATABASE);
4041                }
4042            }
4043        };
4044
4045        // Detect patterns based on filter flags
4046        let show_if_else = !args.r#match; // Show if/else unless --match only
4047        let show_match = !args.if_else; // Show match unless --if-else only
4048
4049        let if_else_patterns = if show_if_else {
4050            detect_if_else_patterns(&cfg)
4051        } else {
4052            vec![]
4053        };
4054
4055        let match_patterns = if show_match {
4056            detect_match_patterns(&cfg)
4057        } else {
4058            vec![]
4059        };
4060
4061        // Convert to response format
4062        let if_else_infos: Vec<IfElseInfo> = if_else_patterns
4063            .iter()
4064            .map(|p| IfElseInfo {
4065                condition_block: cfg[p.condition].id,
4066                true_branch: cfg[p.true_branch].id,
4067                false_branch: cfg[p.false_branch].id,
4068                merge_point: p.merge_point.map(|n| cfg[n].id),
4069                has_else: p.has_else(),
4070            })
4071            .collect();
4072
4073        let match_infos: Vec<MatchInfo> = match_patterns
4074            .iter()
4075            .map(|p| MatchInfo {
4076                switch_block: cfg[p.switch_node].id,
4077                branch_count: p.branch_count(),
4078                targets: p.targets.iter().map(|n| cfg[*n].id).collect(),
4079                otherwise: cfg[p.otherwise].id,
4080            })
4081            .collect();
4082
4083        // Output based on format
4084        match cli.output {
4085            OutputFormat::Human => {
4086                println!("Function: {}", args.function);
4087                println!();
4088
4089                if show_if_else {
4090                    println!("If/Else Patterns: {}", if_else_patterns.len());
4091                    if if_else_patterns.is_empty() {
4092                        output::info("No if/else patterns detected");
4093                    } else {
4094                        for (i, info) in if_else_infos.iter().enumerate() {
4095                            println!("  Pattern {}:", i + 1);
4096                            println!("    Condition: Block {}", info.condition_block);
4097                            println!("    True branch: Block {}", info.true_branch);
4098                            println!("    False branch: Block {}", info.false_branch);
4099                            if let Some(merge) = info.merge_point {
4100                                println!("    Merge point: Block {}", merge);
4101                                println!("    Has else: {}", info.has_else);
4102                            } else {
4103                                println!("    Merge point: None (no else)");
4104                            }
4105                            println!();
4106                        }
4107                    }
4108                    println!();
4109                }
4110
4111                if show_match {
4112                    println!("Match Patterns: {}", match_patterns.len());
4113                    if match_patterns.is_empty() {
4114                        output::info("No match patterns detected");
4115                    } else {
4116                        for (i, info) in match_infos.iter().enumerate() {
4117                            println!("  Pattern {}:", i + 1);
4118                            println!("    Switch: Block {}", info.switch_block);
4119                            println!("    Branch count: {}", info.branch_count);
4120                            println!("    Targets: {:?}", info.targets);
4121                            println!("    Otherwise: Block {}", info.otherwise);
4122                            println!();
4123                        }
4124                    }
4125                }
4126            }
4127            OutputFormat::Json | OutputFormat::Pretty => {
4128                let response = PatternsResponse {
4129                    function: args.function.clone(),
4130                    if_else_count: if_else_patterns.len(),
4131                    match_count: match_patterns.len(),
4132                    if_else_patterns: if_else_infos,
4133                    match_patterns: match_infos,
4134                };
4135                let wrapper = output::JsonResponse::new(response);
4136                match cli.output {
4137                    OutputFormat::Json => println!("{}", wrapper.to_json()),
4138                    OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4139                    _ => unreachable!(),
4140                }
4141            }
4142        }
4143
4144        Ok(())
4145    }
4146
4147    pub fn frontiers(args: &FrontiersArgs, cli: &Cli) -> Result<()> {
4148        use crate::cfg::{compute_dominance_frontiers, DominatorTree};
4149        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
4150        use crate::storage::MirageDb;
4151
4152        // Resolve database path
4153        let db_path = super::resolve_db_path(cli.db.clone())?;
4154
4155        // Open database (follows status command pattern for error handling)
4156        let db = match MirageDb::open(&db_path) {
4157            Ok(db) => db,
4158            Err(_e) => {
4159                // JSON-aware error handling with remediation
4160                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4161                    let error = output::JsonError::database_not_found(&db_path);
4162                    let wrapper = output::JsonResponse::new(error);
4163                    println!("{}", wrapper.to_json());
4164                    std::process::exit(output::EXIT_DATABASE);
4165                } else {
4166                    output::error(&format!("Failed to open database: {}", db_path));
4167                    output::info("Hint: Run 'magellan watch' to create the database");
4168                    std::process::exit(output::EXIT_DATABASE);
4169                }
4170            }
4171        };
4172
4173        // Resolve function name/ID to function_id (with optional file filter)
4174        let function_id =
4175            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4176                Ok(id) => id,
4177                Err(_e) => {
4178                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4179                        let error = output::JsonError::function_not_found(&args.function);
4180                        let wrapper = output::JsonResponse::new(error);
4181                        println!("{}", wrapper.to_json());
4182                        std::process::exit(output::EXIT_DATABASE);
4183                    } else {
4184                        output::error(&format!(
4185                            "Function '{}' not found in database",
4186                            args.function
4187                        ));
4188                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4189                        std::process::exit(output::EXIT_DATABASE);
4190                    }
4191                }
4192            };
4193
4194        // Load CFG from database
4195        let cfg = match load_cfg_from_db(&db, function_id) {
4196            Ok(cfg) => cfg,
4197            Err(_e) => {
4198                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4199                    let error = output::JsonError::new(
4200                        "CgfLoadError",
4201                        &format!("Failed to load CFG for function '{}'", args.function),
4202                        output::E_CFG_ERROR,
4203                    );
4204                    let wrapper = output::JsonResponse::new(error);
4205                    println!("{}", wrapper.to_json());
4206                    std::process::exit(output::EXIT_DATABASE);
4207                } else {
4208                    output::error(&format!(
4209                        "Failed to load CFG for function '{}'",
4210                        args.function
4211                    ));
4212                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
4213                    std::process::exit(output::EXIT_DATABASE);
4214                }
4215            }
4216        };
4217
4218        // Compute dominator tree
4219        let dom_tree = match DominatorTree::new(&cfg) {
4220            Some(tree) => tree,
4221            None => {
4222                output::error("Could not compute dominator tree (CFG may have no entry blocks)");
4223                std::process::exit(1);
4224            }
4225        };
4226
4227        // Compute dominance frontiers
4228        let frontiers = compute_dominance_frontiers(&cfg, dom_tree);
4229
4230        // Handle query modes based on args
4231        if args.iterated {
4232            // Show iterated dominance frontier
4233            let all_nodes: Vec<petgraph::graph::NodeIndex> = cfg.node_indices().collect();
4234            let iterated_frontier = frontiers.iterated_frontier(&all_nodes);
4235            let iterated_blocks: Vec<usize> =
4236                iterated_frontier.iter().map(|&n| cfg[n].id).collect();
4237
4238            match cli.output {
4239                OutputFormat::Human => {
4240                    println!("Function: {}", args.function);
4241                    println!("Iterated Dominance Frontier:");
4242                    println!("Count: {}", iterated_blocks.len());
4243                    println!();
4244                    if iterated_blocks.is_empty() {
4245                        output::info("No iterated dominance frontier (linear CFG)");
4246                    } else {
4247                        println!("Blocks in iterated frontier:");
4248                        for id in &iterated_blocks {
4249                            println!("  - Block {}", id);
4250                        }
4251                    }
4252                }
4253                OutputFormat::Json | OutputFormat::Pretty => {
4254                    let response = IteratedFrontierResponse {
4255                        function: args.function.clone(),
4256                        iterated_frontier: iterated_blocks,
4257                    };
4258                    let wrapper = output::JsonResponse::new(response);
4259                    match cli.output {
4260                        OutputFormat::Json => println!("{}", wrapper.to_json()),
4261                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4262                        _ => unreachable!(),
4263                    }
4264                }
4265            }
4266        } else if let Some(node_id) = args.node {
4267            // Show frontier for specific node only
4268            let target_node = cfg.node_indices().find(|&n| cfg[n].id == node_id);
4269
4270            let target_node = match target_node {
4271                Some(node) => node,
4272                None => {
4273                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4274                        let error = output::JsonError::block_not_found(node_id);
4275                        let wrapper = output::JsonResponse::new(error);
4276                        println!("{}", wrapper.to_json());
4277                        std::process::exit(1);
4278                    } else {
4279                        output::error(&format!("Block {} not found in CFG", node_id));
4280                        std::process::exit(1);
4281                    }
4282                }
4283            };
4284
4285            let frontier = frontiers.frontier(target_node);
4286            let frontier_blocks: Vec<usize> = frontier.iter().map(|&n| cfg[n].id).collect();
4287
4288            match cli.output {
4289                OutputFormat::Human => {
4290                    println!("Function: {}", args.function);
4291                    println!("Dominance Frontier for Block {}:", node_id);
4292                    println!("Count: {}", frontier_blocks.len());
4293                    println!();
4294                    if frontier_blocks.is_empty() {
4295                        output::info(&format!("Block {} has empty dominance frontier", node_id));
4296                    } else {
4297                        println!("Frontier blocks:");
4298                        for id in &frontier_blocks {
4299                            println!("  - Block {}", id);
4300                        }
4301                    }
4302                }
4303                OutputFormat::Json | OutputFormat::Pretty => {
4304                    let response = FrontiersResponse {
4305                        function: args.function.clone(),
4306                        nodes_with_frontiers: if frontier_blocks.is_empty() { 0 } else { 1 },
4307                        frontiers: vec![NodeFrontier {
4308                            node: node_id,
4309                            frontier_set: frontier_blocks,
4310                        }],
4311                    };
4312                    let wrapper = output::JsonResponse::new(response);
4313                    match cli.output {
4314                        OutputFormat::Json => println!("{}", wrapper.to_json()),
4315                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4316                        _ => unreachable!(),
4317                    }
4318                }
4319            }
4320        } else {
4321            // Show all nodes with non-empty frontiers
4322            let nodes_with_frontiers: Vec<NodeFrontier> = frontiers
4323                .nodes_with_frontiers()
4324                .map(|n| {
4325                    let frontier = frontiers.frontier(n);
4326                    NodeFrontier {
4327                        node: cfg[n].id,
4328                        frontier_set: frontier.iter().map(|&f| cfg[f].id).collect(),
4329                    }
4330                })
4331                .collect();
4332
4333            match cli.output {
4334                OutputFormat::Human => {
4335                    println!("Function: {}", args.function);
4336                    println!(
4337                        "Nodes with non-empty dominance frontiers: {}",
4338                        nodes_with_frontiers.len()
4339                    );
4340                    println!();
4341
4342                    if nodes_with_frontiers.is_empty() {
4343                        output::info("No dominance frontiers (linear CFG)");
4344                    } else {
4345                        for node_info in &nodes_with_frontiers {
4346                            println!("Block {}:", node_info.node);
4347                            println!("  Frontier: {:?}", node_info.frontier_set);
4348                            println!();
4349                        }
4350                    }
4351                }
4352                OutputFormat::Json | OutputFormat::Pretty => {
4353                    let response = FrontiersResponse {
4354                        function: args.function.clone(),
4355                        nodes_with_frontiers: nodes_with_frontiers.len(),
4356                        frontiers: nodes_with_frontiers,
4357                    };
4358                    let wrapper = output::JsonResponse::new(response);
4359                    match cli.output {
4360                        OutputFormat::Json => println!("{}", wrapper.to_json()),
4361                        OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
4362                        _ => unreachable!(),
4363                    }
4364                }
4365            }
4366        }
4367
4368        Ok(())
4369    }
4370
4371    pub fn diff(args: &DiffArgs, cli: &Cli) -> Result<()> {
4372        use crate::cfg::diff::compute_cfg_diff;
4373        use crate::storage::MirageDb;
4374
4375        // Resolve database path
4376        let db_path = super::resolve_db_path(cli.db.clone())?;
4377
4378        // Open database
4379        let db = match MirageDb::open(&db_path) {
4380            Ok(db) => db,
4381            Err(_e) => {
4382                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4383                    let error = output::JsonError::database_not_found(&db_path);
4384                    let wrapper = output::JsonResponse::new(error);
4385                    println!("{}", wrapper.to_json());
4386                    std::process::exit(output::EXIT_DATABASE);
4387                } else {
4388                    output::error(&format!("Failed to open database: {}", db_path));
4389                    output::info("Hint: Run 'magellan watch' to create the database");
4390                    std::process::exit(output::EXIT_DATABASE);
4391                }
4392            }
4393        };
4394
4395        // Resolve function name/ID to function_id
4396        let function_id = match db.resolve_function_name(&args.function) {
4397            Ok(id) => id,
4398            Err(e) => {
4399                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4400                    let error = output::JsonError::new("Database", &e.to_string(), "E001");
4401                    let wrapper = output::JsonResponse::new(error);
4402                    println!("{}", wrapper.to_json());
4403                    std::process::exit(output::EXIT_DATABASE);
4404                } else {
4405                    output::error(&format!("Failed to resolve function: {}", e));
4406                    std::process::exit(output::EXIT_DATABASE);
4407                }
4408            }
4409        };
4410
4411        // Compute diff
4412        let diff = match compute_cfg_diff(db.storage(), function_id, &args.before, &args.after) {
4413            Ok(diff) => diff,
4414            Err(e) => {
4415                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4416                    let error = output::JsonError::new("Database", &e.to_string(), "E001");
4417                    let wrapper = output::JsonResponse::new(error);
4418                    println!("{}", wrapper.to_json());
4419                    std::process::exit(output::EXIT_DATABASE);
4420                } else {
4421                    return Err(e);
4422                }
4423            }
4424        };
4425
4426        // Output based on format
4427        match cli.output {
4428            OutputFormat::Human => print_diff_human(&diff, args.show_edges, args.verbose),
4429            OutputFormat::Json => {
4430                let wrapper = output::JsonResponse::new(diff);
4431                println!("{}", wrapper.to_json());
4432            }
4433            OutputFormat::Pretty => {
4434                let wrapper = output::JsonResponse::new(diff);
4435                println!("{}", wrapper.to_pretty_json());
4436            }
4437        }
4438
4439        Ok(())
4440    }
4441
4442    fn print_diff_human(diff: &crate::cfg::diff::CfgDiff, show_edges: bool, verbose: bool) {
4443        use crate::output::{info, success, warn};
4444
4445        info(&format!("CFG Diff: {}", diff.function_name));
4446        println!("  Before: {}", diff.before_snapshot);
4447        println!("  After: {}", diff.after_snapshot);
4448
4449        // Color-code similarity
4450        let similarity_pct = diff.structural_similarity * 100.0;
4451        if similarity_pct >= 90.0 {
4452            success(&format!("  Similarity: {:.1}%", similarity_pct));
4453        } else if similarity_pct >= 70.0 {
4454            println!("  Similarity: {:.1}%", similarity_pct);
4455        } else {
4456            warn(&format!("  Similarity: {:.1}%", similarity_pct));
4457        }
4458
4459        if !diff.added_blocks.is_empty() {
4460            println!();
4461            info(&format!("Added blocks ({}):", diff.added_blocks.len()));
4462            for block in &diff.added_blocks {
4463                println!(
4464                    "  + Block {}: {} @ {}",
4465                    block.block_id, block.kind, block.source_location
4466                );
4467            }
4468        }
4469
4470        if !diff.deleted_blocks.is_empty() {
4471            println!();
4472            info(&format!("Deleted blocks ({}):", diff.deleted_blocks.len()));
4473            for block in &diff.deleted_blocks {
4474                println!(
4475                    "  - Block {}: {} @ {}",
4476                    block.block_id, block.kind, block.source_location
4477                );
4478            }
4479        }
4480
4481        if !diff.modified_blocks.is_empty() && verbose {
4482            println!();
4483            info(&format!(
4484                "Modified blocks ({}):",
4485                diff.modified_blocks.len()
4486            ));
4487            for change in &diff.modified_blocks {
4488                match &change.change_type {
4489                    crate::cfg::diff::ChangeType::TerminatorChanged { before, after } => {
4490                        println!("  ~ Block {}: {} -> {}", change.block_id, before, after);
4491                    }
4492                    crate::cfg::diff::ChangeType::SourceLocationChanged => {
4493                        println!("  ~ Block {}: location changed", change.block_id);
4494                    }
4495                    crate::cfg::diff::ChangeType::BothChanged => {
4496                        println!(
4497                            "  ~ Block {}: terminator and location changed",
4498                            change.block_id
4499                        );
4500                    }
4501                    crate::cfg::diff::ChangeType::EdgesChanged => {
4502                        println!("  ~ Block {}: edges changed", change.block_id);
4503                    }
4504                }
4505            }
4506        }
4507
4508        if show_edges {
4509            if !diff.added_edges.is_empty() {
4510                println!();
4511                info(&format!("Added edges ({}):", diff.added_edges.len()));
4512                for edge in &diff.added_edges {
4513                    println!(
4514                        "  + {} -> {} ({})",
4515                        edge.from_block, edge.to_block, edge.edge_type
4516                    );
4517                }
4518            }
4519            if !diff.deleted_edges.is_empty() {
4520                println!();
4521                info(&format!("Deleted edges ({}):", diff.deleted_edges.len()));
4522                for edge in &diff.deleted_edges {
4523                    println!(
4524                        "  - {} -> {} ({})",
4525                        edge.from_block, edge.to_block, edge.edge_type
4526                    );
4527                }
4528            }
4529        }
4530
4531        // Summary if no changes
4532        if diff.added_blocks.is_empty()
4533            && diff.deleted_blocks.is_empty()
4534            && diff.modified_blocks.is_empty()
4535            && diff.added_edges.is_empty()
4536            && diff.deleted_edges.is_empty()
4537        {
4538            println!();
4539            success("No changes detected");
4540        }
4541    }
4542
4543    pub fn icfg(args: &IcfgArgs, cli: &Cli) -> Result<()> {
4544        use crate::cfg::icfg::{build_icfg, to_dot, IcfgJson, IcfgOptions};
4545        use crate::output::error;
4546        use crate::output::{EXIT_DATABASE, EXIT_NOT_FOUND};
4547        use crate::storage::MirageDb;
4548
4549        let db_path = super::resolve_db_path(cli.db.clone())?;
4550
4551        // Open database
4552        let db = match MirageDb::open(&db_path) {
4553            Ok(db) => db,
4554            Err(e) => {
4555                error(&format!("Failed to open database: {}", e));
4556                std::process::exit(EXIT_DATABASE);
4557            }
4558        };
4559
4560        // Resolve function name to ID
4561        let function_id = match db.resolve_function_name(&args.entry) {
4562            Ok(id) => id,
4563            Err(_) => {
4564                error(&format!("Function not found: {}", args.entry));
4565                std::process::exit(EXIT_NOT_FOUND);
4566            }
4567        };
4568
4569        // Build options
4570        let options = IcfgOptions {
4571            max_depth: args.depth,
4572            include_return_edges: args.return_edges,
4573        };
4574
4575        // Build ICFG
4576        let icfg = match build_icfg(db.storage(), db.backend(), function_id, options) {
4577            Ok(icfg) => icfg,
4578            Err(e) => {
4579                error(&format!("Failed to build ICFG: {}", e));
4580                std::process::exit(EXIT_DATABASE);
4581            }
4582        };
4583
4584        // Output based on format
4585        let format = args.format.unwrap_or(match cli.output {
4586            OutputFormat::Human => IcfgFormat::Human,
4587            _ => IcfgFormat::Dot,
4588        });
4589
4590        match format {
4591            IcfgFormat::Dot => {
4592                println!("{}", to_dot(&icfg));
4593            }
4594            IcfgFormat::Json => {
4595                let json_repr = IcfgJson::from_icfg(&icfg);
4596                println!("{}", serde_json::to_string_pretty(&json_repr)?);
4597            }
4598            IcfgFormat::Human => {
4599                print_icfg_human(&icfg);
4600            }
4601        }
4602
4603        Ok(())
4604    }
4605
4606    fn print_icfg_human(icfg: &crate::cfg::icfg::Icfg) {
4607        use std::collections::HashSet;
4608        println!("Inter-Procedural CFG");
4609        println!("  Entry function: {}", icfg.entry_function);
4610
4611        // Count unique functions
4612        let mut functions = HashSet::new();
4613        for node in icfg.graph.node_indices() {
4614            functions.insert(icfg.graph[node].function_id);
4615        }
4616        println!("  Functions: {}", functions.len());
4617        println!("  Nodes: {}", icfg.graph.node_count());
4618        println!("  Edges: {}", icfg.graph.edge_count());
4619
4620        // Count edge types
4621        let mut call_count = 0;
4622        let mut return_count = 0;
4623        let mut intra_count = 0;
4624
4625        for edge in icfg.graph.edge_indices() {
4626            match &icfg.graph[edge] {
4627                crate::cfg::icfg::IcfgEdge::Call { .. } => call_count += 1,
4628                crate::cfg::icfg::IcfgEdge::Return { .. } => return_count += 1,
4629                crate::cfg::icfg::IcfgEdge::IntraProcedural { .. } => intra_count += 1,
4630            }
4631        }
4632
4633        println!("  Edges by type:");
4634        println!("    Call: {}", call_count);
4635        println!("    Return: {}", return_count);
4636        println!("    Intra-procedural: {}", intra_count);
4637    }
4638
4639    pub fn migrate(args: &MigrateArgs, cli: &Cli) -> Result<()> {
4640        use crate::storage::BackendFormat as StorageBackendFormat;
4641
4642        let db_path = std::path::Path::new(&args.db);
4643
4644        // Validate database exists
4645        if !db_path.exists() {
4646            return Err(anyhow::anyhow!("Database not found: {}", args.db));
4647        }
4648
4649        // Detect actual backend format using mirage's detection
4650        let actual_format = StorageBackendFormat::detect(db_path)
4651            .map_err(|e| anyhow::anyhow!("Backend detection failed: {}", e))?;
4652
4653        // Convert storage BackendFormat to cli BackendFormat for comparison
4654        let actual_format_cli = match actual_format {
4655            StorageBackendFormat::SQLite => BackendFormat::Sqlite,
4656            StorageBackendFormat::Geometric => BackendFormat::Geometric,
4657            StorageBackendFormat::Unknown => {
4658                return Err(anyhow::anyhow!(
4659                    "Cannot detect backend format: unknown format"
4660                ));
4661            }
4662        };
4663
4664        // Validate source format matches actual database
4665        if args.from != actual_format_cli {
4666            return Err(anyhow::anyhow!(
4667                "Source backend mismatch: expected {}, found {:?}",
4668                args.from,
4669                actual_format
4670            ));
4671        }
4672
4673        // Validate source and target are different
4674        if args.from == args.to {
4675            return Err(anyhow::anyhow!(
4676                "Source and target backends must be different"
4677            ));
4678        }
4679
4680        // Dry run: just report what would happen
4681        if args.dry_run {
4682            match cli.output {
4683                OutputFormat::Human => {
4684                    println!("Dry run: would migrate {} -> {}", args.from, args.to);
4685                    println!("Database: {}", args.db);
4686                }
4687                OutputFormat::Json | OutputFormat::Pretty => {
4688                    let output = serde_json::json!({
4689                        "dry_run": true,
4690                        "from": args.from.to_string(),
4691                        "to": args.to.to_string(),
4692                        "database": args.db,
4693                    });
4694                    match cli.output {
4695                        OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
4696                        OutputFormat::Pretty => {
4697                            println!("{}", serde_json::to_string_pretty(&output)?)
4698                        }
4699                        _ => unreachable!(),
4700                    }
4701                }
4702            }
4703            return Ok(());
4704        }
4705
4706        // Create backup if requested
4707        if args.backup {
4708            let timestamp = std::time::SystemTime::now()
4709                .duration_since(std::time::UNIX_EPOCH)
4710                .map(|d| d.as_secs())
4711                .unwrap_or_else(|_| {
4712                    // Fallback to simple counter if clock is before UNIX_EPOCH
4713                    std::time::SystemTime::now()
4714                        .elapsed()
4715                        .map(|d| d.as_secs())
4716                        .unwrap_or(0)
4717                });
4718            let backup_path = format!("{}.backup.{}", args.db, timestamp);
4719            std::fs::copy(&args.db, &backup_path)
4720                .map_err(|e| anyhow::anyhow!("Failed to create backup: {}", e))?;
4721            eprintln!("Backup created: {}", backup_path);
4722        }
4723
4724        Err(anyhow::anyhow!(
4725            "Backend migration is not supported by this Mirage build; use SQLite .magellan/<project>.db databases"
4726        ))
4727    }
4728    pub fn coverage(args: &CoverageArgs, cli: &Cli) -> Result<()> {
4729        use crate::cfg::{load_cfg_from_db, resolve_function_name_with_file};
4730        use crate::storage::MirageDb;
4731
4732        // Resolve database path
4733        let db_path = super::resolve_db_path(cli.db.clone())?;
4734
4735        // Open database
4736        let db = match MirageDb::open(&db_path) {
4737            Ok(db) => db,
4738            Err(_e) => {
4739                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4740                    let error = output::JsonError::database_not_found(&db_path);
4741                    let wrapper = output::JsonResponse::new(error);
4742                    println!("{}", wrapper.to_json());
4743                    std::process::exit(output::EXIT_DATABASE);
4744                } else {
4745                    output::error(&format!("Failed to open database: {}", db_path));
4746                    output::info("Hint: Run 'magellan watch' to create the database");
4747                    std::process::exit(output::EXIT_DATABASE);
4748                }
4749            }
4750        };
4751
4752        // Resolve function name/ID
4753        let function_id =
4754            match resolve_function_name_with_file(&db, &args.function, args.file.as_deref()) {
4755                Ok(id) => id,
4756                Err(_e) => {
4757                    if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4758                        let error = output::JsonError::function_not_found(&args.function);
4759                        let wrapper = output::JsonResponse::new(error);
4760                        println!("{}", wrapper.to_json());
4761                        std::process::exit(output::EXIT_DATABASE);
4762                    } else {
4763                        output::error(&format!(
4764                            "Function '{}' not found in database",
4765                            args.function
4766                        ));
4767                        output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
4768                        std::process::exit(output::EXIT_DATABASE);
4769                    }
4770                }
4771            };
4772
4773        // Load CFG to map block IDs
4774        let cfg = match load_cfg_from_db(&db, function_id) {
4775            Ok(cfg) => cfg,
4776            Err(_e) => {
4777                if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
4778                    let error = output::JsonError::new(
4779                        "CgfLoadError",
4780                        &format!("Failed to load CFG for function '{}'", args.function),
4781                        output::E_CFG_ERROR,
4782                    );
4783                    let wrapper = output::JsonResponse::new(error);
4784                    println!("{}", wrapper.to_json());
4785                    std::process::exit(output::EXIT_DATABASE);
4786                } else {
4787                    output::error(&format!(
4788                        "Failed to load CFG for function '{}'",
4789                        args.function
4790                    ));
4791                    output::info("The function may be corrupted. Try re-running 'magellan watch'");
4792                    std::process::exit(output::EXIT_DATABASE);
4793                }
4794            }
4795        };
4796
4797        // Query coverage data (graceful fallback if tables absent)
4798        let coverage_rows: Vec<(usize, String, i64)> =
4799            db.conn().ok().map_or_else(Vec::new, |conn| {
4800                let sql = "SELECT bb.id, bb.kind, COALESCE(bc.hit_count, 0) \
4801                       FROM cfg_blocks bb \
4802                       LEFT JOIN cfg_block_coverage bc ON bb.id = bc.block_id \
4803                       WHERE bb.function_id = ?1 \
4804                       ORDER BY bb.byte_start";
4805                let mut stmt = match conn.prepare(sql) {
4806                    Ok(s) => s,
4807                    Err(_) => return Vec::new(),
4808                };
4809                let rows = stmt.query_map([function_id], |row| {
4810                    Ok((
4811                        row.get::<_, i64>(0)? as usize,
4812                        row.get::<_, String>(1)?,
4813                        row.get::<_, i64>(2)?,
4814                    ))
4815                });
4816                match rows {
4817                    Ok(iter) => iter.filter_map(|r| r.ok()).collect(),
4818                    Err(_) => Vec::new(),
4819                }
4820            });
4821
4822        // Build a lookup from db_block_id to graph node index (via BasicBlock.id)
4823        let db_id_to_graph_id: std::collections::HashMap<i64, usize> = cfg
4824            .node_indices()
4825            .filter_map(|idx| {
4826                cfg.node_weight(idx)
4827                    .and_then(|b| b.db_id.map(|db_id| (db_id, b.id)))
4828            })
4829            .collect();
4830
4831        match cli.output {
4832            OutputFormat::Human => {
4833                println!("Coverage for function '{}'", args.function);
4834                println!("{}", "=".repeat(60));
4835                if coverage_rows.is_empty() {
4836                    println!("No coverage data available.");
4837                    println!("Hint: Run tests with 'cargo test' to generate coverage.");
4838                } else {
4839                    for (db_id, kind, hits) in &coverage_rows {
4840                        let graph_id = db_id_to_graph_id
4841                            .get(&(*db_id as i64))
4842                            .map(|id| id.to_string())
4843                            .unwrap_or_else(|| "?".to_string());
4844                        println!(
4845                            "  Block {:>3} (graph #{}, kind={:>8}): {:>6} hits",
4846                            db_id, graph_id, kind, hits
4847                        );
4848                    }
4849                }
4850            }
4851            OutputFormat::Json | OutputFormat::Pretty => {
4852                #[derive(serde::Serialize)]
4853                struct CoverageEntry {
4854                    block_id: usize,
4855                    graph_id: Option<usize>,
4856                    kind: String,
4857                    hit_count: i64,
4858                }
4859                let entries: Vec<CoverageEntry> = coverage_rows
4860                    .iter()
4861                    .map(|(db_id, kind, hits)| CoverageEntry {
4862                        block_id: *db_id,
4863                        graph_id: db_id_to_graph_id.get(&(*db_id as i64)).copied(),
4864                        kind: kind.to_string(),
4865                        hit_count: *hits,
4866                    })
4867                    .collect();
4868                let response = output::JsonResponse::new(serde_json::json!({
4869                    "function": args.function,
4870                    "coverage": entries,
4871                }));
4872                match cli.output {
4873                    OutputFormat::Json => println!("{}", response.to_json()),
4874                    OutputFormat::Pretty => println!("{}", response.to_pretty_json()),
4875                    _ => unreachable!(),
4876                }
4877            }
4878        }
4879
4880        Ok(())
4881    }
4882}
4883
4884// ============================================================================
4885// Hotpaths Output Helpers
4886// ============================================================================
4887
4888/// Print hot paths in human-readable format
4889fn print_hotpaths_human(hot_paths: &[crate::cfg::hotpaths::HotPath], show_rationale: bool) {
4890    use crate::output;
4891
4892    output::header(&format!("Hot Paths (top {})", hot_paths.len()));
4893
4894    if hot_paths.is_empty() {
4895        output::info("No hot paths found");
4896        return;
4897    }
4898
4899    for (i, hp) in hot_paths.iter().enumerate() {
4900        println!(
4901            "\n{}. Path {} - Score: {:.2}",
4902            i + 1,
4903            hp.path_id,
4904            hp.hotness_score
4905        );
4906
4907        if show_rationale && !hp.rationale.is_empty() {
4908            println!("   Rationale:");
4909            for r in &hp.rationale {
4910                println!("     - {}", r);
4911            }
4912        }
4913
4914        println!("   Blocks: {} blocks", hp.blocks.len());
4915        for (j, block) in hp.blocks.iter().enumerate() {
4916            if j < 5 || j == hp.blocks.len() - 1 {
4917                print!("     {}", block);
4918                if j == 4 && hp.blocks.len() > 6 {
4919                    println!(" ... (+{} more)", hp.blocks.len() - 6);
4920                    break;
4921                } else {
4922                    println!();
4923                }
4924            }
4925        }
4926    }
4927}
4928
4929// ============================================================================
4930// Tests
4931// ============================================================================
4932
4933#[cfg(test)]
4934mod tests {
4935    use super::*;
4936
4937    // Ensure tests don't interfere with each other by clearing env var
4938    fn clear_env() {
4939        std::env::remove_var("MIRAGE_DB");
4940    }
4941
4942    #[test]
4943    fn test_resolve_db_path_no_source() {
4944        clear_env();
4945        // No arg, no env, no discoverable DB -> returns error
4946        // (unless running in a directory with a .db file)
4947        let result = resolve_db_path(None);
4948        // Result depends on whether there's a .db file in current directory
4949        // So we just verify the function doesn't panic
4950        match result {
4951            Ok(path) => {
4952                // If we got a path, it should be a .db or .db file
4953                assert!(
4954                    path.ends_with(".db") || path.ends_with(".db"),
4955                    "Auto-discovered DB should have .db or .db extension"
4956                );
4957            }
4958            Err(_) => {
4959                // Expected when no DB is available
4960            }
4961        }
4962    }
4963
4964    #[test]
4965    fn test_resolve_db_path_with_cli_arg() {
4966        clear_env();
4967        // CLI arg provided -> returns CLI arg
4968        let result = resolve_db_path(Some("/custom/path.db".to_string())).unwrap();
4969        assert_eq!(result, "/custom/path.db");
4970    }
4971
4972    #[test]
4973    fn test_resolve_db_path_with_env_var() {
4974        clear_env();
4975        // Env var set -> returns env var value
4976        std::env::set_var("MIRAGE_DB", "/env/path.db");
4977        let result = resolve_db_path(None).unwrap();
4978        assert_eq!(result, "/env/path.db");
4979        std::env::remove_var("MIRAGE_DB");
4980    }
4981
4982    #[test]
4983    fn test_resolve_db_path_cli_overrides_env() {
4984        clear_env();
4985        // CLI arg should override env var
4986        std::env::set_var("MIRAGE_DB", "/env/path.db");
4987        let result = resolve_db_path(Some("/cli/path.db".to_string())).unwrap();
4988        assert_eq!(result, "/cli/path.db");
4989        std::env::remove_var("MIRAGE_DB");
4990    }
4991}
4992
4993// ============================================================================
4994// cfg() Command Tests
4995// ============================================================================
4996
4997#[cfg(test)]
4998mod cfg_tests {
4999    use super::*;
5000    use crate::cfg::{export_dot, export_json};
5001
5002    /// Test that DOT format output contains expected Graphviz DOT syntax
5003    #[test]
5004    fn test_cfg_dot_format() {
5005        let cfg = cmds::create_test_cfg();
5006        let dot = export_dot(&cfg);
5007
5008        // Verify basic Graphviz DOT structure
5009        assert!(
5010            dot.contains("digraph CFG"),
5011            "DOT output should contain 'digraph CFG'"
5012        );
5013        assert!(
5014            dot.contains("rankdir=TB"),
5015            "DOT output should contain rankdir attribute"
5016        );
5017        assert!(
5018            dot.contains("node [shape=box"),
5019            "DOT output should contain node shape attribute"
5020        );
5021        assert!(
5022            dot.contains("}"),
5023            "DOT output should end with closing brace"
5024        );
5025
5026        // Verify edge syntax
5027        assert!(dot.contains("->"), "DOT output should contain edge arrows");
5028    }
5029
5030    /// Test that JSON format output is valid and contains expected structure
5031    #[test]
5032    fn test_cfg_json_format() {
5033        let cfg = cmds::create_test_cfg();
5034        let function_name = "test_function";
5035        let export = export_json(&cfg, function_name, None);
5036
5037        // Verify function name is included
5038        assert_eq!(
5039            export.function_name, function_name,
5040            "JSON export should include function name"
5041        );
5042
5043        // Verify structure
5044        assert!(
5045            export.entry.is_some(),
5046            "JSON export should have an entry block"
5047        );
5048        assert!(
5049            !export.exits.is_empty(),
5050            "JSON export should have exit blocks"
5051        );
5052        assert!(!export.blocks.is_empty(), "JSON export should have blocks");
5053        assert!(!export.edges.is_empty(), "JSON export should have edges");
5054
5055        // Verify JSON can be serialized
5056        let json_str = serde_json::to_string(&export);
5057        assert!(
5058            json_str.is_ok(),
5059            "JSON export should be serializable to JSON"
5060        );
5061
5062        // Verify JSON contains function name
5063        let json = json_str.unwrap();
5064        assert!(
5065            json.contains(function_name),
5066            "JSON output should contain function name"
5067        );
5068        assert!(
5069            json.contains("\"entry\""),
5070            "JSON output should contain entry field"
5071        );
5072        assert!(
5073            json.contains("\"exits\""),
5074            "JSON output should contain exits field"
5075        );
5076        assert!(
5077            json.contains("\"blocks\""),
5078            "JSON output should contain blocks field"
5079        );
5080        assert!(
5081            json.contains("\"edges\""),
5082            "JSON output should contain edges field"
5083        );
5084    }
5085
5086    /// Test that function name is correctly passed to export_json()
5087    #[test]
5088    fn test_cfg_function_name_in_export() {
5089        let cfg = cmds::create_test_cfg();
5090
5091        // Test with different function names
5092        let test_names = vec!["my_function", "TestFunc", "module::submodule::function"];
5093
5094        for name in test_names {
5095            let export = export_json(&cfg, name, None);
5096            assert_eq!(
5097                export.function_name, name,
5098                "Function name should be preserved in export"
5099            );
5100        }
5101    }
5102
5103    /// Test format fallback when args.format is None (should use cli.output)
5104    #[test]
5105    fn test_cfg_format_fallback() {
5106        // Test that CfgFormat::Human is used when cli.output is Human
5107        let cli_human = Cli {
5108            db: None,
5109            output: OutputFormat::Human,
5110            command: Some(Commands::Cfg(CfgArgs {
5111                function: "test".to_string(),
5112                file: None,
5113                format: None,
5114            })),
5115            detect_backend: false,
5116        };
5117
5118        let cfg_args = match &cli_human.command {
5119            Some(Commands::Cfg(args)) => args,
5120            _ => panic!("Expected Cfg command"),
5121        };
5122
5123        // Simulate the format resolution logic from cfg()
5124        let resolved_format = cfg_args.format.unwrap_or(match cli_human.output {
5125            OutputFormat::Human => CfgFormat::Human,
5126            OutputFormat::Json => CfgFormat::Json,
5127            OutputFormat::Pretty => CfgFormat::Json,
5128        });
5129
5130        assert_eq!(
5131            resolved_format,
5132            CfgFormat::Human,
5133            "Should fall back to Human format"
5134        );
5135
5136        // Test that CfgFormat::Json is used when cli.output is Json
5137        let cli_json = Cli {
5138            db: None,
5139            output: OutputFormat::Json,
5140            command: Some(Commands::Cfg(CfgArgs {
5141                function: "test".to_string(),
5142                file: None,
5143                format: None,
5144            })),
5145            detect_backend: false,
5146        };
5147
5148        let cfg_args_json = match &cli_json.command {
5149            Some(Commands::Cfg(args)) => args,
5150            _ => panic!("Expected Cfg command"),
5151        };
5152
5153        let resolved_format_json = cfg_args_json.format.unwrap_or(match cli_json.output {
5154            OutputFormat::Human => CfgFormat::Human,
5155            OutputFormat::Json => CfgFormat::Json,
5156            OutputFormat::Pretty => CfgFormat::Json,
5157        });
5158
5159        assert_eq!(
5160            resolved_format_json,
5161            CfgFormat::Json,
5162            "Should fall back to Json format"
5163        );
5164    }
5165
5166    /// Test that JsonResponse wrapper wraps CFGExport correctly
5167    #[test]
5168    fn test_cfg_json_response_wrapper() {
5169        use crate::output::JsonResponse;
5170
5171        let cfg = cmds::create_test_cfg();
5172        let export = export_json(&cfg, "wrapped_function", None);
5173        let response = JsonResponse::new(export);
5174
5175        // Verify JsonResponse structure
5176        assert_eq!(response.schema_version, "1.0.1");
5177        assert_eq!(response.tool, "mirage");
5178        assert!(!response.execution_id.is_empty());
5179        assert!(!response.timestamp.is_empty());
5180
5181        // Verify can be serialized
5182        let json = response.to_json();
5183        assert!(json.contains("\"schema_version\""));
5184        assert!(json.contains("\"execution_id\""));
5185        assert!(json.contains("\"tool\":\"mirage\""));
5186        assert!(json.contains("\"data\""));
5187        assert!(json.contains("wrapped_function"));
5188    }
5189
5190    /// Test DOT format contains expected block information
5191    #[test]
5192    fn test_cfg_dot_block_info() {
5193        let cfg = cmds::create_test_cfg();
5194        let dot = export_dot(&cfg);
5195
5196        // Check for ENTRY block marker (green fill)
5197        assert!(
5198            dot.contains("lightgreen"),
5199            "DOT should mark entry block with green"
5200        );
5201
5202        // Check for EXIT block marker (coral fill)
5203        assert!(
5204            dot.contains("lightcoral"),
5205            "DOT should mark exit blocks with coral"
5206        );
5207
5208        // Check for block labels
5209        assert!(dot.contains("Block"), "DOT should contain block labels");
5210    }
5211
5212    /// Test DOT format contains expected edge information
5213    #[test]
5214    fn test_cfg_dot_edge_info() {
5215        let cfg = cmds::create_test_cfg();
5216        let dot = export_dot(&cfg);
5217
5218        // Check for edge colors (TrueBranch=green, FalseBranch=red)
5219        assert!(
5220            dot.contains("color=green"),
5221            "DOT should show true branch edges in green"
5222        );
5223        assert!(
5224            dot.contains("color=red"),
5225            "DOT should show false branch edges in red"
5226        );
5227    }
5228}
5229
5230// ============================================================================
5231// status() Command Tests
5232// ============================================================================
5233
5234#[cfg(test)]
5235mod status_tests {
5236    use crate::storage::{create_schema, MirageDb};
5237    use rusqlite::{params, Connection};
5238
5239    /// Create a test database with sample data
5240    fn create_test_db() -> anyhow::Result<(tempfile::NamedTempFile, MirageDb)> {
5241        use crate::storage::{
5242            REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
5243        };
5244
5245        let file = tempfile::NamedTempFile::new()?;
5246        let mut conn = Connection::open(file.path())?;
5247
5248        // Create Magellan tables (simplified)
5249        conn.execute(
5250            "CREATE TABLE magellan_meta (
5251                id INTEGER PRIMARY KEY CHECK (id = 1),
5252                magellan_schema_version INTEGER NOT NULL,
5253                sqlitegraph_schema_version INTEGER NOT NULL,
5254                created_at INTEGER NOT NULL
5255            )",
5256            [],
5257        )?;
5258
5259        conn.execute(
5260            "CREATE TABLE graph_entities (
5261                id INTEGER PRIMARY KEY AUTOINCREMENT,
5262                kind TEXT NOT NULL,
5263                name TEXT NOT NULL,
5264                file_path TEXT,
5265                data TEXT NOT NULL
5266            )",
5267            [],
5268        )?;
5269
5270        conn.execute(
5271            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5272             VALUES (1, ?, ?, ?)",
5273            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
5274        )?;
5275
5276        // Create Mirage schema
5277        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
5278
5279        // Add sample data
5280        conn.execute(
5281            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
5282            params!("function", "test_func", "test.rs", "{}"),
5283        )?;
5284        let function_id: i64 = conn.last_insert_rowid();
5285
5286        // Add test blocks
5287        conn.execute(
5288            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
5289             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
5290            params!(function_id, "entry", "goto", 0, 10, 1, 0, 1, 10),
5291        )?;
5292        conn.execute(
5293            "INSERT INTO cfg_blocks (function_id, kind, terminator, byte_start, byte_end, start_line, start_col, end_line, end_col)
5294             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
5295            params!(function_id, "return", "return", 10, 20, 2, 0, 2, 10),
5296        )?;
5297
5298        // cfg_edges table is managed by Magellan v11+; Mirage does not create it.
5299        // Edges are reconstructed in memory from terminator data.
5300
5301        // Add test paths
5302        conn.execute(
5303            "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
5304             VALUES (?, ?, ?, ?, ?, ?, ?)",
5305            params!("test_path", function_id, "normal", 1, 2, 2, 0),
5306        )?;
5307
5308        // Add test dominators
5309        conn.execute(
5310            "INSERT INTO cfg_dominators (block_id, dominator_id, is_strict) VALUES (?, ?, ?)",
5311            params!(1, 1, false),
5312        )?;
5313
5314        let db = MirageDb::open(file.path())?;
5315        Ok((file, db))
5316    }
5317
5318    /// Test that status() returns correct database statistics
5319    #[test]
5320    #[cfg(feature = "backend-sqlite")]
5321    #[allow(deprecated)]
5322    fn test_status_returns_correct_statistics() {
5323        let (_file, db) = create_test_db().unwrap();
5324        let status = db.status().unwrap();
5325
5326        assert_eq!(status.cfg_blocks, 2, "Should have 2 cfg_blocks");
5327        // cfg_edges is managed by Magellan v11+; count may be 0 if table absent
5328        assert_eq!(
5329            status.cfg_edges, 0,
5330            "cfg_edges count should be 0 (managed by Magellan)"
5331        );
5332        assert_eq!(status.cfg_paths, 1, "Should have 1 cfg_path");
5333        assert_eq!(status.cfg_dominators, 1, "Should have 1 cfg_dominator");
5334        assert_eq!(
5335            status.mirage_schema_version, 1,
5336            "Schema version should be 1"
5337        );
5338        assert_eq!(
5339            status.magellan_schema_version, 7,
5340            "Magellan version should be 7"
5341        );
5342    }
5343
5344    /// Test that human output format contains expected fields
5345    #[test]
5346    #[cfg(feature = "backend-sqlite")]
5347    #[allow(deprecated)]
5348    fn test_status_human_output_format() {
5349        let (_file, db) = create_test_db().unwrap();
5350        let status = db.status().unwrap();
5351
5352        // Verify all expected fields are present and have correct values
5353        assert!(status.cfg_blocks >= 0, "cfg_blocks should be non-negative");
5354        assert!(status.cfg_edges >= 0, "cfg_edges should be non-negative");
5355        assert!(status.cfg_paths >= 0, "cfg_paths should be non-negative");
5356        assert!(
5357            status.cfg_dominators >= 0,
5358            "cfg_dominators should be non-negative"
5359        );
5360        assert!(
5361            status.mirage_schema_version > 0,
5362            "mirage_schema_version should be positive"
5363        );
5364        assert!(
5365            status.magellan_schema_version > 0,
5366            "magellan_schema_version should be positive"
5367        );
5368    }
5369
5370    /// Test that JSON output format is valid and contains expected structure
5371    #[test]
5372    #[cfg(feature = "backend-sqlite")]
5373    fn test_status_json_output_format() {
5374        use crate::output::JsonResponse;
5375
5376        let (_file, db) = create_test_db().unwrap();
5377        let status = db.status().unwrap();
5378        let response = JsonResponse::new(status);
5379
5380        // Verify JsonResponse wrapper structure
5381        assert_eq!(response.schema_version, "1.0.1");
5382        assert_eq!(response.tool, "mirage");
5383        assert!(!response.execution_id.is_empty());
5384        assert!(!response.timestamp.is_empty());
5385
5386        // Verify JSON serialization
5387        let json = response.to_json();
5388        assert!(json.contains("\"schema_version\":\"1.0.1\""));
5389        assert!(json.contains("\"tool\":\"mirage\""));
5390        assert!(json.contains("\"execution_id\""));
5391        assert!(json.contains("\"timestamp\""));
5392        assert!(json.contains("\"data\""));
5393        assert!(json.contains("\"cfg_blocks\""));
5394        assert!(json.contains("\"cfg_edges\""));
5395        assert!(json.contains("\"cfg_paths\""));
5396        assert!(json.contains("\"cfg_dominators\""));
5397        assert!(json.contains("\"mirage_schema_version\""));
5398        assert!(json.contains("\"magellan_schema_version\""));
5399    }
5400
5401    /// Test that pretty JSON output is formatted with indentation
5402    #[test]
5403    #[cfg(feature = "backend-sqlite")]
5404    fn test_status_pretty_json_output_format() {
5405        use crate::output::JsonResponse;
5406
5407        let (_file, db) = create_test_db().unwrap();
5408        let status = db.status().unwrap();
5409        let response = JsonResponse::new(status);
5410
5411        let pretty_json = response.to_pretty_json();
5412
5413        // Pretty JSON should contain newlines and indentation
5414        assert!(
5415            pretty_json.contains("\n"),
5416            "Pretty JSON should contain newlines"
5417        );
5418        assert!(
5419            pretty_json.contains("  "),
5420            "Pretty JSON should contain indentation"
5421        );
5422
5423        // Should still be valid JSON
5424        let parsed: serde_json::Value =
5425            serde_json::from_str(&pretty_json).expect("Pretty JSON should be valid");
5426        assert!(parsed.is_object(), "Parsed JSON should be an object");
5427        assert_eq!(parsed["schema_version"], "1.0.1");
5428        assert_eq!(parsed["tool"], "mirage");
5429        assert!(parsed["data"].is_object(), "data field should be an object");
5430    }
5431
5432    /// Test that database open error is handled correctly
5433    #[test]
5434    fn test_status_database_open_error() {
5435        use crate::storage::MirageDb;
5436
5437        // Try to open a non-existent database
5438        let result = MirageDb::open("/nonexistent/path/to/database.db");
5439
5440        // Use match to check error without Debug requirement
5441        match result {
5442            Ok(_) => panic!("Should fail to open non-existent database"),
5443            Err(e) => {
5444                let err_msg = e.to_string();
5445                assert!(
5446                    err_msg.contains("Database not found") || err_msg.contains("not found"),
5447                    "Error message should mention database not found: {}",
5448                    err_msg
5449                );
5450            }
5451        }
5452    }
5453
5454    /// Test that status() with empty database returns zero counts
5455    #[test]
5456    #[cfg(feature = "backend-sqlite")]
5457    #[allow(deprecated)]
5458    fn test_status_empty_database_returns_zeros() {
5459        use crate::storage::{
5460            REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
5461        };
5462
5463        let file = tempfile::NamedTempFile::new().unwrap();
5464        let mut conn = Connection::open(file.path()).unwrap();
5465
5466        // Create minimal schema
5467        conn.execute(
5468            "CREATE TABLE magellan_meta (
5469                id INTEGER PRIMARY KEY CHECK (id = 1),
5470                magellan_schema_version INTEGER NOT NULL,
5471                sqlitegraph_schema_version INTEGER NOT NULL,
5472                created_at INTEGER NOT NULL
5473            )",
5474            [],
5475        )
5476        .unwrap();
5477
5478        conn.execute(
5479            "CREATE TABLE graph_entities (
5480                id INTEGER PRIMARY KEY AUTOINCREMENT,
5481                kind TEXT NOT NULL,
5482                name TEXT NOT NULL,
5483                file_path TEXT,
5484                data TEXT NOT NULL
5485            )",
5486            [],
5487        )
5488        .unwrap();
5489
5490        conn.execute(
5491            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5492             VALUES (1, ?, ?, ?)",
5493            params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
5494        ).unwrap();
5495
5496        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
5497
5498        let db = MirageDb::open(file.path()).unwrap();
5499        let status = db.status().unwrap();
5500
5501        assert_eq!(
5502            status.cfg_blocks, 0,
5503            "Empty database should have 0 cfg_blocks"
5504        );
5505        // cfg_edges table is managed by Magellan v11+; Mirage does not create it
5506        assert_eq!(
5507            status.cfg_edges, 0,
5508            "cfg_edges count should be 0 (table managed by Magellan)"
5509        );
5510        assert_eq!(
5511            status.cfg_paths, 0,
5512            "Empty database should have 0 cfg_paths"
5513        );
5514        assert_eq!(
5515            status.cfg_dominators, 0,
5516            "Empty database should have 0 cfg_dominators"
5517        );
5518    }
5519}
5520
5521// ============================================================================
5522// paths() Command Tests
5523// ============================================================================
5524
5525#[cfg(test)]
5526mod paths_tests {
5527    use super::*;
5528    use crate::cfg::{enumerate_paths, PathKind, PathLimits};
5529
5530    /// Test that paths() command enumerates paths from a test CFG
5531    #[test]
5532    fn test_paths_enumeration_basic() {
5533        let cfg = cmds::create_test_cfg();
5534        let limits = PathLimits::default();
5535        let paths = enumerate_paths(&cfg, &limits);
5536
5537        // Test CFG has 2 paths (entry -> true -> return, entry -> false -> return)
5538        assert!(!paths.is_empty(), "Should find at least one path");
5539        assert_eq!(paths.len(), 2, "Test CFG should have exactly 2 paths");
5540
5541        // Both paths should be Normal kind (no errors in test CFG)
5542        let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
5543        assert_eq!(normal_count, 2, "Both paths should be Normal");
5544    }
5545
5546    /// Test that show_errors flag filters to error paths only
5547    #[test]
5548    fn test_paths_show_errors_filter() {
5549        let cfg = cmds::create_test_cfg();
5550        let limits = PathLimits::default();
5551        let mut paths = enumerate_paths(&cfg, &limits);
5552
5553        // Filter to error paths
5554        paths.retain(|p| p.kind == PathKind::Error);
5555
5556        // Test CFG has no error paths
5557        assert_eq!(paths.len(), 0, "Test CFG should have no error paths");
5558
5559        // Verify filter worked by checking all remaining paths would be errors
5560        for path in &paths {
5561            assert_eq!(
5562                path.kind,
5563                PathKind::Error,
5564                "Filtered paths should all be Error kind"
5565            );
5566        }
5567    }
5568
5569    /// Test that max_length limit is applied to path enumeration
5570    #[test]
5571    fn test_paths_max_length_limit() {
5572        let cfg = cmds::create_test_cfg();
5573
5574        // Set a very low max_length limit
5575        let limits = PathLimits::default().with_max_length(1);
5576        let paths = enumerate_paths(&cfg, &limits);
5577
5578        // All paths should have length <= 1
5579        for path in &paths {
5580            assert!(path.len() <= 1, "Path length should be <= max_length limit");
5581        }
5582
5583        // With max_length=1, we should get fewer paths than unrestricted
5584        let unlimited_paths = enumerate_paths(&cfg, &PathLimits::default());
5585        assert!(
5586            paths.len() <= unlimited_paths.len(),
5587            "Limited enumeration should produce <= paths than unlimited"
5588        );
5589    }
5590
5591    /// Test that PathsArgs.function is extracted correctly
5592    #[test]
5593    fn test_paths_args_function_extraction() {
5594        let args = PathsArgs {
5595            function: "test_function".to_string(),
5596            file: None,
5597            show_errors: false,
5598            max_length: None,
5599            with_blocks: false,
5600            incremental: false,
5601            since: None,
5602            by_coverage: false,
5603        };
5604
5605        assert_eq!(args.function, "test_function");
5606        assert!(!args.show_errors);
5607        assert!(args.max_length.is_none());
5608        assert!(!args.with_blocks);
5609    }
5610
5611    /// Test that PathsArgs with flags set correctly reflects state
5612    #[test]
5613    fn test_paths_args_with_flags() {
5614        let args = PathsArgs {
5615            function: "my_func".to_string(),
5616            file: None,
5617            show_errors: true,
5618            max_length: Some(10),
5619            with_blocks: true,
5620            incremental: false,
5621            since: None,
5622            by_coverage: false,
5623        };
5624
5625        assert_eq!(args.function, "my_func");
5626        assert!(args.show_errors, "show_errors flag should be true");
5627        assert_eq!(args.max_length, Some(10), "max_length should be Some(10)");
5628        assert!(args.with_blocks, "with_blocks flag should be true");
5629    }
5630
5631    /// Test PathSummary conversion from Path
5632    #[test]
5633    fn test_path_summary_from_path() {
5634        use crate::cfg::Path;
5635
5636        let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5637        let summary = PathSummary::from(path);
5638
5639        assert!(!summary.path_id.is_empty(), "path_id should not be empty");
5640        assert_eq!(summary.kind, "Normal", "kind should match PathKind");
5641        assert_eq!(summary.length, 3, "length should match path length");
5642
5643        // blocks is now Vec<PathBlock> with block_id and terminator
5644        assert_eq!(summary.blocks.len(), 3, "should have 3 blocks");
5645        assert_eq!(summary.blocks[0].block_id, 0, "first block_id should be 0");
5646        assert_eq!(summary.blocks[1].block_id, 1, "second block_id should be 1");
5647        assert_eq!(summary.blocks[2].block_id, 2, "third block_id should be 2");
5648        assert_eq!(
5649            summary.blocks[0].terminator, "Unknown",
5650            "terminator should be Unknown placeholder"
5651        );
5652
5653        // Optional fields should be None until populated in future plans
5654        assert!(summary.summary.is_none(), "summary should be None");
5655        assert!(
5656            summary.source_range.is_none(),
5657            "source_range should be None"
5658        );
5659    }
5660
5661    /// Test PathSummary conversion for different PathKinds
5662    #[test]
5663    fn test_path_summary_different_kinds() {
5664        use crate::cfg::Path;
5665
5666        let kinds = vec![
5667            (PathKind::Normal, "Normal"),
5668            (PathKind::Error, "Error"),
5669            (PathKind::Degenerate, "Degenerate"),
5670            (PathKind::Unreachable, "Unreachable"),
5671        ];
5672
5673        for (kind, expected_str) in kinds {
5674            let path = Path::new(vec![0, 1], kind);
5675            let summary = PathSummary::from(path);
5676            assert_eq!(
5677                summary.kind, expected_str,
5678                "PathKind::{:?} should serialize to {}",
5679                kind, expected_str
5680            );
5681        }
5682    }
5683
5684    /// Test that multiple paths produce multiple PathSummaries
5685    #[test]
5686    fn test_paths_response_multiple_paths() {
5687        use crate::cfg::Path;
5688
5689        let paths = vec![
5690            Path::new(vec![0, 1], PathKind::Normal),
5691            Path::new(vec![0, 2], PathKind::Normal),
5692            Path::new(vec![0, 1, 3], PathKind::Error),
5693        ];
5694
5695        let summaries: Vec<PathSummary> = paths.into_iter().map(PathSummary::from).collect();
5696
5697        assert_eq!(summaries.len(), 3, "Should have 3 summaries");
5698
5699        // Check that error path is correctly identified
5700        let error_summaries = summaries.iter().filter(|s| s.kind == "Error").count();
5701        assert_eq!(error_summaries, 1, "Should have 1 error path");
5702    }
5703
5704    /// Test PathsResponse contains expected metadata
5705    #[test]
5706    fn test_paths_response_metadata() {
5707        let response = PathsResponse {
5708            function: "test_func".to_string(),
5709            total_paths: 5,
5710            error_paths: 2,
5711            paths: vec![],
5712        };
5713
5714        assert_eq!(response.function, "test_func");
5715        assert_eq!(response.total_paths, 5);
5716        assert_eq!(response.error_paths, 2);
5717        assert!(response.paths.is_empty());
5718    }
5719
5720    /// Test integration: create_test_cfg produces enumerable paths
5721    #[test]
5722    fn test_paths_integration_with_test_cfg() {
5723        let cfg = cmds::create_test_cfg();
5724        let limits = PathLimits::default();
5725        let paths = enumerate_paths(&cfg, &limits);
5726
5727        // Verify we got the expected number of paths for the diamond CFG
5728        assert!(!paths.is_empty(), "Test CFG should produce paths");
5729
5730        // Each path should start at entry (block 0)
5731        for path in &paths {
5732            assert_eq!(path.blocks[0], 0, "All paths should start at entry block 0");
5733            assert_eq!(path.entry, 0, "Path entry should be block 0");
5734        }
5735
5736        // Each path should end at an exit block
5737        for path in &paths {
5738            assert!(
5739                path.exit == 2 || path.exit == 3,
5740                "Path exit should be either block 2 or 3 (the return blocks)"
5741            );
5742        }
5743    }
5744
5745    /// Test that with_blocks flag affects output format (integration check)
5746    #[test]
5747    fn test_paths_args_with_blocks_flag() {
5748        let args_with = PathsArgs {
5749            function: "test".to_string(),
5750            file: None,
5751            show_errors: false,
5752            max_length: None,
5753            with_blocks: true,
5754            incremental: false,
5755            since: None,
5756            by_coverage: false,
5757        };
5758
5759        let args_without = PathsArgs {
5760            function: "test".to_string(),
5761            file: None,
5762            show_errors: false,
5763            max_length: None,
5764            with_blocks: false,
5765            incremental: false,
5766            since: None,
5767            by_coverage: false,
5768        };
5769
5770        assert!(args_with.with_blocks, "with_blocks should be true");
5771        assert!(!args_without.with_blocks, "with_blocks should be false");
5772    }
5773
5774    /// Test PathSummary::from_with_cfg with source locations
5775    #[test]
5776    fn test_path_summary_from_with_cfg() {
5777        use crate::cfg::{
5778            BasicBlock, BlockKind, EdgeType, Path, PathKind, SourceLocation, Terminator,
5779        };
5780        use petgraph::graph::DiGraph;
5781        use std::path::PathBuf;
5782
5783        // Create a test CFG with source locations
5784        let mut g = DiGraph::new();
5785
5786        let loc0 = SourceLocation {
5787            file_path: PathBuf::from("test.rs"),
5788            byte_start: 0,
5789            byte_end: 10,
5790            start_line: 1,
5791            start_column: 1,
5792            end_line: 1,
5793            end_column: 10,
5794        };
5795
5796        let loc1 = SourceLocation {
5797            file_path: PathBuf::from("test.rs"),
5798            byte_start: 11,
5799            byte_end: 20,
5800            start_line: 2,
5801            start_column: 1,
5802            end_line: 2,
5803            end_column: 10,
5804        };
5805
5806        let loc2 = SourceLocation {
5807            file_path: PathBuf::from("test.rs"),
5808            byte_start: 21,
5809            byte_end: 30,
5810            start_line: 3,
5811            start_column: 1,
5812            end_line: 3,
5813            end_column: 10,
5814        };
5815
5816        let b0 = g.add_node(BasicBlock {
5817            id: 0,
5818            db_id: None,
5819            kind: BlockKind::Entry,
5820            statements: vec!["let x = 1".to_string()],
5821            terminator: Terminator::Goto { target: 1 },
5822            source_location: Some(loc0),
5823            coord_x: 0,
5824            coord_y: 0,
5825            coord_z: 0,
5826        });
5827
5828        let b1 = g.add_node(BasicBlock {
5829            id: 1,
5830            db_id: None,
5831            kind: BlockKind::Normal,
5832            statements: vec!["if x > 0".to_string()],
5833            terminator: Terminator::SwitchInt {
5834                targets: vec![2],
5835                otherwise: 2,
5836            },
5837            source_location: Some(loc1),
5838            coord_x: 0,
5839            coord_y: 0,
5840            coord_z: 0,
5841        });
5842
5843        let b2 = g.add_node(BasicBlock {
5844            id: 2,
5845            db_id: None,
5846            kind: BlockKind::Exit,
5847            statements: vec!["return true".to_string()],
5848            terminator: Terminator::Return,
5849            source_location: Some(loc2),
5850            coord_x: 0,
5851            coord_y: 0,
5852            coord_z: 0,
5853        });
5854
5855        g.add_edge(b0, b1, EdgeType::Fallthrough);
5856        g.add_edge(b1, b2, EdgeType::TrueBranch);
5857
5858        // Create a path and use from_with_cfg
5859        let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5860        let summary = PathSummary::from_with_cfg(path, &g);
5861
5862        // Check terminator is populated
5863        assert_eq!(summary.blocks[0].terminator, "Goto { target: 1 }");
5864        assert!(summary.blocks[1].terminator.contains("SwitchInt"));
5865        assert_eq!(summary.blocks[2].terminator, "Return");
5866
5867        // Check source_range is populated
5868        assert!(
5869            summary.source_range.is_some(),
5870            "source_range should be Some"
5871        );
5872        let sr = summary.source_range.as_ref().unwrap();
5873        assert_eq!(sr.file_path, "test.rs");
5874        assert_eq!(sr.start_line, 1);
5875        assert_eq!(sr.end_line, 3);
5876    }
5877
5878    /// Test PathSummary::from_with_cfg with no source locations (graceful None)
5879    #[test]
5880    fn test_path_summary_from_with_cfg_no_source_locations() {
5881        use crate::cfg::{Path, PathKind};
5882
5883        // Use the test CFG which has no source locations
5884        let cfg = cmds::create_test_cfg();
5885        let path = Path::new(vec![0, 1, 2], PathKind::Normal);
5886        let summary = PathSummary::from_with_cfg(path, &cfg);
5887
5888        // Terminator should still be populated
5889        assert!(summary.blocks[0].terminator.contains("Goto"));
5890        assert!(summary.blocks[1].terminator.contains("SwitchInt"));
5891        assert_eq!(summary.blocks[2].terminator, "Return");
5892
5893        // source_range should be None when no source locations exist
5894        assert!(
5895            summary.source_range.is_none(),
5896            "source_range should be None when CFG has no locations"
5897        );
5898    }
5899
5900    // ------------------------------------------------------------------------
5901    // Path Caching Tests
5902    // ------------------------------------------------------------------------
5903
5904    /// Test that first call enumerates paths (cache miss)
5905    #[test]
5906    fn test_paths_cache_miss_first_call() {
5907        use crate::cfg::get_or_enumerate_paths;
5908        use crate::storage::create_schema;
5909        use rusqlite::Connection;
5910
5911        // Create an in-memory database with Mirage schema
5912        let mut conn = Connection::open_in_memory().unwrap();
5913
5914        // Create Magellan schema first (required for Mirage schema)
5915        conn.execute(
5916            "CREATE TABLE magellan_meta (
5917                id INTEGER PRIMARY KEY CHECK (id = 1),
5918                magellan_schema_version INTEGER NOT NULL,
5919                sqlitegraph_schema_version INTEGER NOT NULL,
5920                created_at INTEGER NOT NULL
5921            )",
5922            [],
5923        )
5924        .unwrap();
5925
5926        conn.execute(
5927            "CREATE TABLE graph_entities (
5928                id INTEGER PRIMARY KEY AUTOINCREMENT,
5929                kind TEXT NOT NULL,
5930                name TEXT NOT NULL,
5931                file_path TEXT,
5932                data TEXT NOT NULL
5933            )",
5934            [],
5935        )
5936        .unwrap();
5937
5938        conn.execute(
5939            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
5940             VALUES (1, 4, 3, 0)",
5941            [],
5942        ).unwrap();
5943
5944        // Create Mirage schema
5945        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
5946
5947        // Get test CFG and limits
5948        let cfg = cmds::create_test_cfg();
5949        let limits = PathLimits::default();
5950        let test_function_id: i64 = 1; // First auto-increment ID;
5951                                       // Insert a test function entity (required for foreign key constraint)
5952        conn.execute(
5953            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
5954            rusqlite::params!("function", "test_func", "test.rs", "{}"),
5955        )
5956        .unwrap();
5957
5958        // Enable foreign key enforcement
5959        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
5960        let test_function_hash: &str = "test_cfg";
5961
5962        // First call should enumerate (no cache)
5963        let paths1 = get_or_enumerate_paths(
5964            &cfg,
5965            test_function_id,
5966            test_function_hash,
5967            &limits,
5968            &mut conn,
5969        )
5970        .unwrap();
5971
5972        // Verify we got paths
5973        assert!(
5974            !paths1.is_empty(),
5975            "First call should enumerate and return paths"
5976        );
5977        assert_eq!(paths1.len(), 2, "Test CFG should have 2 paths");
5978
5979        // Verify paths were stored in database
5980        let path_count: i64 = conn
5981            .query_row(
5982                "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
5983                rusqlite::params![test_function_id],
5984                |row| row.get(0),
5985            )
5986            .unwrap();
5987
5988        assert_eq!(
5989            path_count, 2,
5990            "Paths should be stored in database after first call"
5991        );
5992
5993        // Note: function_hash verification removed - not available in Magellan schema
5994    }
5995
5996    /// Test that second call returns cached paths (cache hit)
5997    #[test]
5998    fn test_paths_cache_hit_second_call() {
5999        use crate::cfg::get_or_enumerate_paths;
6000        use crate::storage::create_schema;
6001        use rusqlite::Connection;
6002
6003        // Create an in-memory database with Mirage schema
6004        let mut conn = Connection::open_in_memory().unwrap();
6005
6006        // Create Magellan schema first
6007        conn.execute(
6008            "CREATE TABLE magellan_meta (
6009                id INTEGER PRIMARY KEY CHECK (id = 1),
6010                magellan_schema_version INTEGER NOT NULL,
6011                sqlitegraph_schema_version INTEGER NOT NULL,
6012                created_at INTEGER NOT NULL
6013            )",
6014            [],
6015        )
6016        .unwrap();
6017
6018        conn.execute(
6019            "CREATE TABLE graph_entities (
6020                id INTEGER PRIMARY KEY AUTOINCREMENT,
6021                kind TEXT NOT NULL,
6022                name TEXT NOT NULL,
6023                file_path TEXT,
6024                data TEXT NOT NULL
6025            )",
6026            [],
6027        )
6028        .unwrap();
6029
6030        conn.execute(
6031            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6032             VALUES (1, 4, 3, 0)",
6033            [],
6034        ).unwrap();
6035
6036        // Create Mirage schema
6037        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
6038        // Insert a test function entity (required for foreign key constraint)
6039        conn.execute(
6040            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6041            rusqlite::params!("function", "test_func", "test.rs", "{}"),
6042        )
6043        .unwrap();
6044
6045        // Enable foreign key enforcement
6046        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
6047
6048        // Get test CFG and limits
6049        let cfg = cmds::create_test_cfg();
6050        let limits = PathLimits::default();
6051        let test_function_id: i64 = 1; // First auto-increment ID;
6052        let test_function_hash: &str = "test_cfg";
6053
6054        // First call - cache miss, enumerates and stores
6055        let paths1 = get_or_enumerate_paths(
6056            &cfg,
6057            test_function_id,
6058            test_function_hash,
6059            &limits,
6060            &mut conn,
6061        )
6062        .unwrap();
6063
6064        // Verify paths were stored
6065        let path_count: i64 = conn
6066            .query_row(
6067                "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6068                rusqlite::params![test_function_id],
6069                |row| row.get(0),
6070            )
6071            .unwrap();
6072        assert_eq!(path_count, 2, "Should have 2 paths stored after first call");
6073
6074        // Second call - cache hit, should return same paths
6075        let paths2 = get_or_enumerate_paths(
6076            &cfg,
6077            test_function_id,
6078            test_function_hash,
6079            &limits,
6080            &mut conn,
6081        )
6082        .unwrap();
6083
6084        // Should return same number of paths
6085        assert_eq!(
6086            paths2.len(),
6087            paths1.len(),
6088            "Cache hit should return same number of paths"
6089        );
6090
6091        // Paths should have identical path_ids (cache hit returns same data)
6092        let mut path_ids1: Vec<_> = paths1.iter().map(|p| &p.path_id).collect();
6093        let mut path_ids2: Vec<_> = paths2.iter().map(|p| &p.path_id).collect();
6094        path_ids1.sort();
6095        path_ids2.sort();
6096
6097        assert_eq!(
6098            path_ids1, path_ids2,
6099            "Cache hit should return paths with same IDs"
6100        );
6101
6102        // Verify path entries match
6103        for (p1, p2) in paths1.iter().zip(paths2.iter()) {
6104            assert_eq!(p1.path_id, p2.path_id, "Path IDs should match on cache hit");
6105            assert_eq!(p1.kind, p2.kind, "Path kinds should match on cache hit");
6106            assert_eq!(
6107                p1.blocks, p2.blocks,
6108                "Path blocks should match on cache hit"
6109            );
6110        }
6111    }
6112
6113    /// Test that function hash change invalidates cache
6114    #[test]
6115    fn test_paths_cache_invalidation_on_hash_change() {
6116        use crate::cfg::get_or_enumerate_paths;
6117        use crate::storage::create_schema;
6118        use rusqlite::Connection;
6119
6120        // Create an in-memory database with Mirage schema
6121        let mut conn = Connection::open_in_memory().unwrap();
6122
6123        // Create Magellan schema first
6124        conn.execute(
6125            "CREATE TABLE magellan_meta (
6126                id INTEGER PRIMARY KEY CHECK (id = 1),
6127                magellan_schema_version INTEGER NOT NULL,
6128                sqlitegraph_schema_version INTEGER NOT NULL,
6129                created_at INTEGER NOT NULL
6130            )",
6131            [],
6132        )
6133        .unwrap();
6134
6135        conn.execute(
6136            "CREATE TABLE graph_entities (
6137                id INTEGER PRIMARY KEY AUTOINCREMENT,
6138                kind TEXT NOT NULL,
6139                name TEXT NOT NULL,
6140                file_path TEXT,
6141                data TEXT NOT NULL
6142            )",
6143            [],
6144        )
6145        .unwrap();
6146
6147        conn.execute(
6148            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6149             VALUES (1, 4, 3, 0)",
6150            [],
6151        ).unwrap();
6152
6153        // Create Mirage schema
6154        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
6155        // Insert a test function entity (required for foreign key constraint)
6156        conn.execute(
6157            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6158            rusqlite::params!("function", "test_func", "test.rs", "{}"),
6159        )
6160        .unwrap();
6161
6162        // Enable foreign key enforcement
6163        conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
6164
6165        // Get test CFG and limits
6166        let cfg = cmds::create_test_cfg();
6167        let limits = PathLimits::default();
6168        let test_function_id: i64 = 1; // First auto-increment ID;
6169        let test_function_hash_v1: &str = "test_cfg_v1";
6170        let test_function_hash_v3: &str = "test_cfg_v3";
6171
6172        // First call with hash v1 - cache miss, enumerates and stores
6173        let paths1 = get_or_enumerate_paths(
6174            &cfg,
6175            test_function_id,
6176            test_function_hash_v1,
6177            &limits,
6178            &mut conn,
6179        )
6180        .unwrap();
6181
6182        // Verify paths were stored
6183        let path_count_v1: i64 = conn
6184            .query_row(
6185                "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6186                rusqlite::params![test_function_id],
6187                |row| row.get(0),
6188            )
6189            .unwrap();
6190
6191        assert_eq!(path_count_v1, 2, "Should have 2 paths after first call");
6192
6193        // Second call with different hash - cache invalidation, should re-enumerate
6194        // Note: With Magellan schema, hash-based caching is not available
6195        // Paths are always invalidated and re-stored on each call
6196        let paths2 = get_or_enumerate_paths(
6197            &cfg,
6198            test_function_id,
6199            test_function_hash_v3,
6200            &limits,
6201            &mut conn,
6202        )
6203        .unwrap();
6204
6205        // Should still return paths (re-enumerated)
6206        assert!(!paths2.is_empty(), "Should re-enumerate");
6207        assert_eq!(
6208            paths2.len(),
6209            paths1.len(),
6210            "Re-enumeration should produce same paths"
6211        );
6212
6213        // Verify paths were updated (old invalidated, new stored)
6214        let path_count: i64 = conn
6215            .query_row(
6216                "SELECT COUNT(*) FROM cfg_paths WHERE function_id = ?",
6217                rusqlite::params![test_function_id],
6218                |row| row.get(0),
6219            )
6220            .unwrap();
6221
6222        assert_eq!(path_count, 2, "Should have 2 paths after re-enumeration");
6223    }
6224}
6225
6226// ============================================================================
6227// unreachable() Command Tests
6228// ============================================================================
6229
6230#[cfg(test)]
6231mod unreachable_tests {
6232    use super::*;
6233    use crate::cfg::reachability::find_unreachable;
6234    use crate::cfg::{BasicBlock, BlockKind, Cfg, EdgeType, Terminator};
6235    use petgraph::graph::DiGraph;
6236
6237    /// Helper to create a test CFG with an unreachable block
6238    fn create_cfg_with_unreachable() -> Cfg {
6239        let mut g = DiGraph::new();
6240
6241        // Block 0: entry, goes to 1
6242        let b0 = g.add_node(BasicBlock {
6243            id: 0,
6244            db_id: None,
6245            kind: BlockKind::Entry,
6246            statements: vec!["let x = 1".to_string()],
6247            terminator: Terminator::Goto { target: 1 },
6248            source_location: None,
6249            coord_x: 0,
6250            coord_y: 0,
6251            coord_z: 0,
6252        });
6253
6254        // Block 1: normal, goes to 2
6255        let b1 = g.add_node(BasicBlock {
6256            id: 1,
6257            db_id: None,
6258            kind: BlockKind::Normal,
6259            statements: vec!["if x > 0".to_string()],
6260            terminator: Terminator::SwitchInt {
6261                targets: vec![2],
6262                otherwise: 3,
6263            },
6264            source_location: None,
6265            coord_x: 0,
6266            coord_y: 0,
6267            coord_z: 0,
6268        });
6269
6270        // Block 2: exit (reachable)
6271        let b2 = g.add_node(BasicBlock {
6272            id: 2,
6273            db_id: None,
6274            kind: BlockKind::Exit,
6275            statements: vec!["return true".to_string()],
6276            terminator: Terminator::Return,
6277            source_location: None,
6278            coord_x: 0,
6279            coord_y: 0,
6280            coord_z: 0,
6281        });
6282
6283        // Block 3: exit (reachable)
6284        let b3 = g.add_node(BasicBlock {
6285            id: 3,
6286            db_id: None,
6287            kind: BlockKind::Exit,
6288            statements: vec!["return false".to_string()],
6289            terminator: Terminator::Return,
6290            source_location: None,
6291            coord_x: 0,
6292            coord_y: 0,
6293            coord_z: 0,
6294        });
6295
6296        // Block 4: unreachable (no edges to it)
6297        let _b4 = g.add_node(BasicBlock {
6298            id: 4,
6299            db_id: None,
6300            kind: BlockKind::Exit,
6301            statements: vec!["unreachable code".to_string()],
6302            terminator: Terminator::Unreachable,
6303            source_location: None,
6304            coord_x: 0,
6305            coord_y: 0,
6306            coord_z: 0,
6307        });
6308
6309        g.add_edge(b0, b1, EdgeType::Fallthrough);
6310        g.add_edge(b1, b2, EdgeType::TrueBranch);
6311        g.add_edge(b1, b3, EdgeType::FalseBranch);
6312
6313        g
6314    }
6315
6316    /// Test that unreachable blocks are detected
6317    #[test]
6318    fn test_unreachable_detects_dead_code() {
6319        let cfg = create_cfg_with_unreachable();
6320        let unreachable_indices = find_unreachable(&cfg);
6321
6322        // Should find exactly 1 unreachable block (block 4)
6323        assert_eq!(
6324            unreachable_indices.len(),
6325            1,
6326            "Should find exactly 1 unreachable block"
6327        );
6328
6329        // Verify it's block 4
6330        let block_id = cfg.node_weight(unreachable_indices[0]).unwrap().id;
6331        assert_eq!(block_id, 4, "Unreachable block should be block 4");
6332    }
6333
6334    /// Test that UnreachableResponse struct serializes correctly
6335    #[test]
6336    fn test_unreachable_response_serialization() {
6337        use crate::output::JsonResponse;
6338
6339        let response = UnreachableResponse {
6340            uncalled_functions: None,
6341            function: "test_func".to_string(),
6342            total_functions: 1,
6343            functions_with_unreachable: 1,
6344            unreachable_count: 1,
6345            blocks: vec![UnreachableBlock {
6346                block_id: 4,
6347                kind: "Exit".to_string(),
6348                statements: vec!["unreachable code".to_string()],
6349                terminator: "Unreachable".to_string(),
6350                incoming_edges: vec![],
6351            }],
6352        };
6353
6354        let wrapper = JsonResponse::new(response);
6355        let json = wrapper.to_json();
6356
6357        assert!(json.contains("\"function\":\"test_func\""));
6358        assert!(json.contains("\"unreachable_count\":1"));
6359        assert!(json.contains("\"block_id\":4"));
6360        assert!(json.contains("\"kind\":\"Exit\""));
6361    }
6362
6363    /// Test that empty unreachable response is handled correctly
6364    #[test]
6365    fn test_unreachable_empty_response() {
6366        use crate::output::JsonResponse;
6367
6368        let response = UnreachableResponse {
6369            uncalled_functions: None,
6370            function: "test_func".to_string(),
6371            total_functions: 1,
6372            functions_with_unreachable: 0,
6373            unreachable_count: 0,
6374            blocks: vec![],
6375        };
6376
6377        let wrapper = JsonResponse::new(response);
6378        let json = wrapper.to_json();
6379
6380        assert!(json.contains("\"unreachable_count\":0"));
6381        assert!(json.contains("\"functions_with_unreachable\":0"));
6382    }
6383
6384    /// Test that UnreachableBlock struct contains expected fields
6385    #[test]
6386    fn test_unreachable_block_fields() {
6387        let block = UnreachableBlock {
6388            block_id: 5,
6389            kind: "Normal".to_string(),
6390            statements: vec!["stmt1".to_string(), "stmt2".to_string()],
6391            terminator: "Return".to_string(),
6392            incoming_edges: vec![],
6393        };
6394
6395        assert_eq!(block.block_id, 5);
6396        assert_eq!(block.kind, "Normal");
6397        assert_eq!(block.statements.len(), 2);
6398        assert_eq!(block.terminator, "Return");
6399    }
6400
6401    /// Test UnreachableArgs flags
6402    #[test]
6403    fn test_unreachable_args_flags() {
6404        let args_with = UnreachableArgs {
6405            include_uncalled: false,
6406            within_functions: true,
6407            show_branches: true,
6408        };
6409
6410        let args_without = UnreachableArgs {
6411            include_uncalled: false,
6412            within_functions: false,
6413            show_branches: false,
6414        };
6415
6416        assert!(args_with.within_functions);
6417        assert!(args_with.show_branches);
6418        assert!(!args_without.within_functions);
6419        assert!(!args_without.show_branches);
6420    }
6421
6422    /// Test that create_test_cfg has no unreachable blocks
6423    #[test]
6424    fn test_test_cfg_fully_reachable() {
6425        let cfg = cmds::create_test_cfg();
6426        let unreachable_indices = find_unreachable(&cfg);
6427
6428        // Test CFG should have no unreachable blocks
6429        assert_eq!(
6430            unreachable_indices.len(),
6431            0,
6432            "Test CFG should have no unreachable blocks"
6433        );
6434    }
6435
6436    /// Test that --show-branches includes incoming edge details
6437    #[test]
6438    fn test_unreachable_show_branches_with_edges() {
6439        use crate::cfg::reachability::find_unreachable;
6440        use petgraph::visit::EdgeRef;
6441
6442        // Create a CFG with an unreachable block that HAS incoming edges
6443        // This simulates a block that's only reachable from an unreachable source
6444        let mut g = DiGraph::new();
6445
6446        let b0 = g.add_node(BasicBlock {
6447            id: 0,
6448            db_id: None,
6449            kind: BlockKind::Entry,
6450            statements: vec!["let x = 1".to_string()],
6451            terminator: Terminator::Goto { target: 1 },
6452            source_location: None,
6453            coord_x: 0,
6454            coord_y: 0,
6455            coord_z: 0,
6456        });
6457
6458        let b1 = g.add_node(BasicBlock {
6459            id: 1,
6460            db_id: None,
6461            kind: BlockKind::Normal,
6462            statements: vec!["if x > 0".to_string()],
6463            terminator: Terminator::SwitchInt {
6464                targets: vec![2],
6465                otherwise: 3,
6466            },
6467            source_location: None,
6468            coord_x: 0,
6469            coord_y: 0,
6470            coord_z: 0,
6471        });
6472
6473        let b2 = g.add_node(BasicBlock {
6474            id: 2,
6475            db_id: None,
6476            kind: BlockKind::Exit,
6477            statements: vec!["return true".to_string()],
6478            terminator: Terminator::Return,
6479            source_location: None,
6480            coord_x: 0,
6481            coord_y: 0,
6482            coord_z: 0,
6483        });
6484
6485        // b3 and b4 are both unreachable, but b4 has an incoming edge from b3
6486        let b3 = g.add_node(BasicBlock {
6487            id: 3,
6488            db_id: None,
6489            kind: BlockKind::Normal,
6490            statements: vec!["unreachable branch".to_string()],
6491            terminator: Terminator::Goto { target: 4 },
6492            source_location: None,
6493            coord_x: 0,
6494            coord_y: 0,
6495            coord_z: 0,
6496        });
6497
6498        let b4 = g.add_node(BasicBlock {
6499            id: 4,
6500            db_id: None,
6501            kind: BlockKind::Exit,
6502            statements: vec!["unreachable code".to_string()],
6503            terminator: Terminator::Unreachable,
6504            source_location: None,
6505            coord_x: 0,
6506            coord_y: 0,
6507            coord_z: 0,
6508        });
6509
6510        // Only connect entry to b1, making b3 and b4 unreachable
6511        g.add_edge(b0, b1, EdgeType::Fallthrough);
6512        g.add_edge(b1, b2, EdgeType::TrueBranch);
6513        // b3 -> b4 edge exists, but both blocks are unreachable
6514        g.add_edge(b3, b4, EdgeType::Fallthrough);
6515
6516        // Build UnreachableBlock structs with show_branches=true
6517        let unreachable_indices = find_unreachable(&g);
6518        let blocks: Vec<UnreachableBlock> = unreachable_indices
6519            .iter()
6520            .map(|&idx| {
6521                let block = &g[idx];
6522                let kind_str = format!("{:?}", block.kind);
6523                let terminator_str = format!("{:?}", block.terminator);
6524
6525                // Collect incoming edges
6526                let incoming_edges: Vec<IncomingEdge> = g
6527                    .edge_references()
6528                    .filter(|edge| edge.target() == idx)
6529                    .map(|edge| {
6530                        let source_block = &g[edge.source()];
6531                        let edge_type = g.edge_weight(edge.id()).unwrap();
6532                        IncomingEdge {
6533                            from_block: source_block.id,
6534                            edge_type: format!("{:?}", edge_type),
6535                        }
6536                    })
6537                    .collect();
6538
6539                UnreachableBlock {
6540                    block_id: block.id,
6541                    kind: kind_str,
6542                    statements: block.statements.clone(),
6543                    terminator: terminator_str,
6544                    incoming_edges,
6545                }
6546            })
6547            .collect();
6548
6549        // Should find 2 unreachable blocks (3 and 4)
6550        assert_eq!(blocks.len(), 2);
6551
6552        // Block 3 should have no incoming edges (isolated unreachable code)
6553        let block3 = blocks.iter().find(|b| b.block_id == 3).unwrap();
6554        assert_eq!(block3.incoming_edges.len(), 0);
6555
6556        // Block 4 should have 1 incoming edge from block 3
6557        let block4 = blocks.iter().find(|b| b.block_id == 4).unwrap();
6558        assert_eq!(block4.incoming_edges.len(), 1);
6559        assert_eq!(block4.incoming_edges[0].from_block, 3);
6560        assert_eq!(block4.incoming_edges[0].edge_type, "Fallthrough");
6561    }
6562
6563    /// Test that --show-branches JSON output includes incoming_edges field
6564    #[test]
6565    fn test_unreachable_show_branches_json_output() {
6566        use crate::cfg::reachability::find_unreachable;
6567        use crate::output::JsonResponse;
6568        use petgraph::visit::EdgeRef;
6569
6570        // Create the same CFG as above
6571        let mut g = DiGraph::new();
6572
6573        let b0 = g.add_node(BasicBlock {
6574            id: 0,
6575            db_id: None,
6576            kind: BlockKind::Entry,
6577            statements: vec!["let x = 1".to_string()],
6578            terminator: Terminator::Goto { target: 1 },
6579            source_location: None,
6580            coord_x: 0,
6581            coord_y: 0,
6582            coord_z: 0,
6583        });
6584
6585        let b1 = g.add_node(BasicBlock {
6586            id: 1,
6587            db_id: None,
6588            kind: BlockKind::Normal,
6589            statements: vec!["if x > 0".to_string()],
6590            terminator: Terminator::SwitchInt {
6591                targets: vec![2],
6592                otherwise: 3,
6593            },
6594            source_location: None,
6595            coord_x: 0,
6596            coord_y: 0,
6597            coord_z: 0,
6598        });
6599
6600        let b2 = g.add_node(BasicBlock {
6601            id: 2,
6602            db_id: None,
6603            kind: BlockKind::Exit,
6604            statements: vec!["return true".to_string()],
6605            terminator: Terminator::Return,
6606            source_location: None,
6607            coord_x: 0,
6608            coord_y: 0,
6609            coord_z: 0,
6610        });
6611
6612        let b3 = g.add_node(BasicBlock {
6613            id: 3,
6614            db_id: None,
6615            kind: BlockKind::Normal,
6616            statements: vec!["unreachable branch".to_string()],
6617            terminator: Terminator::Goto { target: 4 },
6618            source_location: None,
6619            coord_x: 0,
6620            coord_y: 0,
6621            coord_z: 0,
6622        });
6623
6624        let b4 = g.add_node(BasicBlock {
6625            id: 4,
6626            db_id: None,
6627            kind: BlockKind::Exit,
6628            statements: vec!["unreachable code".to_string()],
6629            terminator: Terminator::Unreachable,
6630            source_location: None,
6631            coord_x: 0,
6632            coord_y: 0,
6633            coord_z: 0,
6634        });
6635
6636        g.add_edge(b0, b1, EdgeType::Fallthrough);
6637        g.add_edge(b1, b2, EdgeType::TrueBranch);
6638        g.add_edge(b3, b4, EdgeType::Fallthrough);
6639
6640        // Build UnreachableBlock structs with incoming edges
6641        let unreachable_indices = find_unreachable(&g);
6642        let blocks: Vec<UnreachableBlock> = unreachable_indices
6643            .iter()
6644            .map(|&idx| {
6645                let block = &g[idx];
6646                UnreachableBlock {
6647                    block_id: block.id,
6648                    kind: format!("{:?}", block.kind),
6649                    statements: block.statements.clone(),
6650                    terminator: format!("{:?}", block.terminator),
6651                    incoming_edges: g
6652                        .edge_references()
6653                        .filter(|edge| edge.target() == idx)
6654                        .map(|edge| {
6655                            let source_block = &g[edge.source()];
6656                            let edge_type = g.edge_weight(edge.id()).unwrap();
6657                            IncomingEdge {
6658                                from_block: source_block.id,
6659                                edge_type: format!("{:?}", edge_type),
6660                            }
6661                        })
6662                        .collect(),
6663                }
6664            })
6665            .collect();
6666
6667        let response = UnreachableResponse {
6668            function: "test".to_string(),
6669            total_functions: 1,
6670            functions_with_unreachable: 1,
6671            unreachable_count: 2,
6672            blocks,
6673            uncalled_functions: None,
6674        };
6675
6676        let wrapper = JsonResponse::new(response);
6677        let json = wrapper.to_json();
6678
6679        // Verify JSON contains incoming_edges field
6680        assert!(json.contains("\"incoming_edges\""));
6681        // Verify block 4 has an incoming edge from block 3
6682        assert!(json.contains("\"from_block\":3"));
6683        assert!(json.contains("\"edge_type\":\"Fallthrough\""));
6684    }
6685
6686    /// Test that IncomingEdge struct serializes correctly
6687    #[test]
6688    fn test_incoming_edge_serialization() {
6689        let edge = IncomingEdge {
6690            from_block: 5,
6691            edge_type: "TrueBranch".to_string(),
6692        };
6693
6694        let serialized = serde_json::to_string(&edge).unwrap();
6695        assert!(serialized.contains("\"from_block\":5"));
6696        assert!(serialized.contains("\"edge_type\":\"TrueBranch\""));
6697    }
6698}
6699
6700// ============================================================================
6701// dominators() Command Tests
6702// ============================================================================
6703
6704#[cfg(test)]
6705mod dominators_tests {
6706    use super::*;
6707    use crate::cfg::{DominatorTree, PostDominatorTree};
6708    use tempfile::NamedTempFile;
6709
6710    /// Create a minimal test database
6711    fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
6712        use crate::storage::{
6713            REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
6714        };
6715        let file = NamedTempFile::new()?;
6716        let conn = rusqlite::Connection::open(file.path())?;
6717
6718        // Create Magellan tables
6719        conn.execute(
6720            "CREATE TABLE magellan_meta (
6721                id INTEGER PRIMARY KEY CHECK (id = 1),
6722                magellan_schema_version INTEGER NOT NULL,
6723                sqlitegraph_schema_version INTEGER NOT NULL,
6724                created_at INTEGER NOT NULL
6725            )",
6726            [],
6727        )?;
6728
6729        conn.execute(
6730            "CREATE TABLE graph_entities (
6731                id INTEGER PRIMARY KEY AUTOINCREMENT,
6732                kind TEXT NOT NULL,
6733                name TEXT NOT NULL,
6734                file_path TEXT,
6735                data TEXT NOT NULL
6736            )",
6737            [],
6738        )?;
6739
6740        conn.execute(
6741            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
6742             VALUES (1, ?, ?, ?)",
6743            rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
6744        )?;
6745
6746        // Create Mirage schema
6747        conn.execute(
6748            "CREATE TABLE mirage_meta (
6749                id INTEGER PRIMARY KEY CHECK (id = 1),
6750                mirage_schema_version INTEGER NOT NULL,
6751                magellan_schema_version INTEGER NOT NULL,
6752                created_at INTEGER NOT NULL
6753            )",
6754            [],
6755        )?;
6756
6757        conn.execute(
6758            "CREATE TABLE cfg_blocks (
6759                id INTEGER PRIMARY KEY AUTOINCREMENT,
6760                function_id INTEGER NOT NULL,
6761                kind TEXT NOT NULL,
6762                byte_start INTEGER NOT NULL,
6763                byte_end INTEGER NOT NULL,
6764                terminator TEXT NOT NULL,
6765                function_hash TEXT NOT NULL
6766            )",
6767            [],
6768        )?;
6769
6770        // cfg_edges table is managed by Magellan v11+; Mirage does not create it.
6771
6772        conn.execute(
6773            "CREATE TABLE cfg_paths (
6774                path_id TEXT PRIMARY KEY,
6775                function_id INTEGER NOT NULL,
6776                path_kind TEXT NOT NULL,
6777                entry_block INTEGER NOT NULL,
6778                exit_block INTEGER NOT NULL,
6779                length INTEGER NOT NULL,
6780                created_at INTEGER NOT NULL
6781            )",
6782            [],
6783        )?;
6784
6785        conn.execute(
6786            "CREATE TABLE cfg_dominators (
6787                id INTEGER PRIMARY KEY AUTOINCREMENT,
6788                block_id INTEGER NOT NULL,
6789                dominator_id INTEGER NOT NULL,
6790                is_strict INTEGER NOT NULL
6791            )",
6792            [],
6793        )?;
6794
6795        conn.execute(
6796            "INSERT INTO mirage_meta (id, mirage_schema_version, magellan_schema_version, created_at)
6797             VALUES (1, 1, 4, 0)",
6798            [],
6799        )?;
6800
6801        // Add a test function
6802        conn.execute(
6803            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
6804            rusqlite::params!("function", "test_func", "test.rs", "{}"),
6805        )?;
6806
6807        Ok(file)
6808    }
6809
6810    /// Test that DominatorTree can be computed from test CFG
6811    #[test]
6812    fn test_dominator_tree_computation() {
6813        let cfg = cmds::create_test_cfg();
6814        let dom_tree = DominatorTree::new(&cfg);
6815
6816        assert!(
6817            dom_tree.is_some(),
6818            "DominatorTree should be computed successfully"
6819        );
6820
6821        let dom_tree = dom_tree.unwrap();
6822        // Entry block (0) should be the root
6823        assert_eq!(cfg[dom_tree.root()].id, 0, "Root should be entry block");
6824    }
6825
6826    /// Test that PostDominatorTree can be computed from test CFG
6827    #[test]
6828    fn test_post_dominator_tree_computation() {
6829        let cfg = cmds::create_test_cfg();
6830        let post_dom_tree = PostDominatorTree::new(&cfg);
6831
6832        assert!(
6833            post_dom_tree.is_some(),
6834            "PostDominatorTree should be computed successfully"
6835        );
6836
6837        let post_dom_tree = post_dom_tree.unwrap();
6838        // Root of post-dominator tree should be an exit block
6839        let root_id = cfg[post_dom_tree.root()].id;
6840        assert!(root_id == 2 || root_id == 3, "Root should be an exit block");
6841    }
6842
6843    /// Test immediate dominator relationships in test CFG
6844    #[test]
6845    fn test_immediate_dominator_relationships() {
6846        let cfg = cmds::create_test_cfg();
6847        let dom_tree = DominatorTree::new(&cfg).unwrap();
6848
6849        // Find nodes by block ID
6850        let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
6851        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6852        let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
6853        let node_3 = cfg.node_indices().find(|&n| cfg[n].id == 3).unwrap();
6854
6855        // Entry (0) has no immediate dominator
6856        assert_eq!(
6857            dom_tree.immediate_dominator(node_0),
6858            None,
6859            "Entry should have no dominator"
6860        );
6861
6862        // Node 1 is dominated by entry (0)
6863        assert_eq!(
6864            dom_tree.immediate_dominator(node_1),
6865            Some(node_0),
6866            "Node 1 should be dominated by entry"
6867        );
6868
6869        // Node 2 is dominated by node 1 (through true branch)
6870        assert_eq!(
6871            dom_tree.immediate_dominator(node_2),
6872            Some(node_1),
6873            "Node 2 should be dominated by node 1"
6874        );
6875
6876        // Node 3 is dominated by node 1 (through false branch)
6877        assert_eq!(
6878            dom_tree.immediate_dominator(node_3),
6879            Some(node_1),
6880            "Node 3 should be dominated by node 1"
6881        );
6882    }
6883
6884    /// Test dominates() method
6885    #[test]
6886    fn test_dominates_method() {
6887        let cfg = cmds::create_test_cfg();
6888        let dom_tree = DominatorTree::new(&cfg).unwrap();
6889
6890        let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
6891        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6892        let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
6893
6894        // Entry dominates all nodes
6895        assert!(dom_tree.dominates(node_0, node_0), "Node dominates itself");
6896        assert!(dom_tree.dominates(node_0, node_1), "Entry dominates node 1");
6897        assert!(dom_tree.dominates(node_0, node_2), "Entry dominates node 2");
6898
6899        // Non-entry doesn't dominate entry
6900        assert!(
6901            !dom_tree.dominates(node_1, node_0),
6902            "Node 1 does not dominate entry"
6903        );
6904    }
6905
6906    /// Test children() method returns dominated nodes
6907    #[test]
6908    fn test_dominator_tree_children() {
6909        let cfg = cmds::create_test_cfg();
6910        let dom_tree = DominatorTree::new(&cfg).unwrap();
6911
6912        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
6913
6914        // Node 1 should have 2 children (blocks 2 and 3)
6915        let children = dom_tree.children(node_1);
6916        assert_eq!(children.len(), 2, "Node 1 should have 2 children");
6917
6918        let child_ids: Vec<_> = children.iter().map(|&n| cfg[n].id).collect();
6919        assert!(child_ids.contains(&2), "Children should include block 2");
6920        assert!(child_ids.contains(&3), "Children should include block 3");
6921    }
6922
6923    /// Test DominatorsArgs struct has expected fields
6924    #[test]
6925    fn test_dominators_args_fields() {
6926        let args = DominatorsArgs {
6927            function: "test_func".to_string(),
6928            file: None,
6929            must_pass_through: Some("1".to_string()),
6930            post: false,
6931            inter_procedural: false,
6932        };
6933
6934        assert_eq!(args.function, "test_func");
6935        assert_eq!(args.must_pass_through, Some("1".to_string()));
6936        assert!(!args.post);
6937        assert!(!args.inter_procedural);
6938    }
6939
6940    /// Test DominatorsArgs with --post flag
6941    #[test]
6942    fn test_dominators_args_with_post_flag() {
6943        let args = DominatorsArgs {
6944            function: "my_function".to_string(),
6945            file: None,
6946            must_pass_through: None,
6947            post: true,
6948            inter_procedural: false,
6949        };
6950
6951        assert_eq!(args.function, "my_function");
6952        assert!(args.post, "post flag should be true");
6953        assert!(
6954            args.must_pass_through.is_none(),
6955            "must_pass_through should be None"
6956        );
6957        assert!(!args.inter_procedural);
6958    }
6959
6960    /// Test DominanceResponse struct serializes correctly
6961    #[test]
6962    fn test_dominance_response_serialization() {
6963        let response = DominanceResponse {
6964            function: "test".to_string(),
6965            kind: "dominators".to_string(),
6966            root: Some(0),
6967            dominance_tree: vec![DominatorEntry {
6968                block: 0,
6969                immediate_dominator: None,
6970                dominated: vec![1],
6971            }],
6972            must_pass_through: None,
6973        };
6974
6975        let json = serde_json::to_string(&response);
6976        assert!(json.is_ok(), "DominanceResponse should serialize to JSON");
6977
6978        let json_str = json.unwrap();
6979        assert!(json_str.contains("\"function\":\"test\""));
6980        assert!(json_str.contains("\"kind\":\"dominators\""));
6981        assert!(json_str.contains("\"root\":0"));
6982    }
6983
6984    /// Test MustPassThroughResult struct
6985    #[test]
6986    fn test_must_pass_through_result() {
6987        let result = MustPassThroughResult {
6988            block: 1,
6989            must_pass: vec![1, 2, 3],
6990        };
6991
6992        assert_eq!(result.block, 1);
6993        assert_eq!(result.must_pass.len(), 3);
6994        assert_eq!(result.must_pass, vec![1, 2, 3]);
6995
6996        // Verify it serializes correctly
6997        let json = serde_json::to_string(&result);
6998        assert!(json.is_ok());
6999        let json_str = json.unwrap();
7000        assert!(json_str.contains("\"block\":1"));
7001        assert!(json_str.contains("\"must_pass\":[1,2,3]"));
7002    }
7003
7004    /// Test DominatorEntry struct
7005    #[test]
7006    fn test_dominator_entry() {
7007        let entry = DominatorEntry {
7008            block: 5,
7009            immediate_dominator: Some(2),
7010            dominated: vec![6, 7],
7011        };
7012
7013        assert_eq!(entry.block, 5);
7014        assert_eq!(entry.immediate_dominator, Some(2));
7015        assert_eq!(entry.dominated, vec![6, 7]);
7016    }
7017
7018    /// Test post-dominates() method
7019    #[test]
7020    fn test_post_dominates_method() {
7021        let cfg = cmds::create_test_cfg();
7022        let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
7023
7024        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7025        let node_2 = cfg.node_indices().find(|&n| cfg[n].id == 2).unwrap();
7026
7027        // Exit post-dominates nodes that can reach it
7028        assert!(
7029            post_dom_tree.post_dominates(node_2, node_2),
7030            "Node post-dominates itself"
7031        );
7032        assert!(
7033            post_dom_tree.post_dominates(node_2, node_1),
7034            "Exit post-dominates node 1"
7035        );
7036    }
7037
7038    /// Test immediate post-dominator relationships
7039    #[test]
7040    fn test_immediate_post_dominator_relationships() {
7041        let cfg = cmds::create_test_cfg();
7042        let post_dom_tree = PostDominatorTree::new(&cfg).unwrap();
7043
7044        let node_0 = cfg.node_indices().find(|&n| cfg[n].id == 0).unwrap();
7045        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7046
7047        // Node 1 should be immediately post-dominated by an exit (2 or 3)
7048        let ipdom_1 = post_dom_tree.immediate_post_dominator(node_1);
7049        assert!(
7050            ipdom_1.is_some(),
7051            "Node 1 should have an immediate post-dominator"
7052        );
7053
7054        // Node 0 should be immediately post-dominated by node 1
7055        let ipdom_0 = post_dom_tree.immediate_post_dominator(node_0);
7056        assert_eq!(
7057            ipdom_0,
7058            Some(node_1),
7059            "Node 0 should be immediately post-dominated by node 1"
7060        );
7061    }
7062
7063    /// Test that empty CFG returns None for DominatorTree
7064    #[test]
7065    fn test_empty_cfg_dominator_tree() {
7066        use petgraph::graph::DiGraph;
7067        let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7068        let dom_tree = DominatorTree::new(&empty_cfg);
7069
7070        assert!(
7071            dom_tree.is_none(),
7072            "Empty CFG should produce None for DominatorTree"
7073        );
7074    }
7075
7076    /// Test that empty CFG returns None for PostDominatorTree
7077    #[test]
7078    fn test_empty_cfg_post_dominator_tree() {
7079        use petgraph::graph::DiGraph;
7080        let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7081        let post_dom_tree = PostDominatorTree::new(&empty_cfg);
7082
7083        assert!(
7084            post_dom_tree.is_none(),
7085            "Empty CFG should produce None for PostDominatorTree"
7086        );
7087    }
7088
7089    /// Test JsonResponse wrapper for DominanceResponse
7090    #[test]
7091    fn test_dominance_response_json_wrapper() {
7092        use crate::output::JsonResponse;
7093
7094        let response = DominanceResponse {
7095            function: "wrapped_test".to_string(),
7096            kind: "dominators".to_string(),
7097            root: Some(0),
7098            dominance_tree: vec![],
7099            must_pass_through: None,
7100        };
7101
7102        let wrapper = JsonResponse::new(response);
7103
7104        assert_eq!(wrapper.schema_version, "1.0.1");
7105        assert_eq!(wrapper.tool, "mirage");
7106        assert!(!wrapper.execution_id.is_empty());
7107        assert!(!wrapper.timestamp.is_empty());
7108
7109        // Verify JSON contains expected fields
7110        let json = wrapper.to_json();
7111        assert!(json.contains("\"schema_version\":\"1.0.1\""));
7112        assert!(json.contains("\"tool\":\"mirage\""));
7113        assert!(json.contains("wrapped_test"));
7114    }
7115
7116    /// Test must-pass-through query with valid block
7117    #[test]
7118    fn test_must_pass_through_valid_block() {
7119        let cfg = cmds::create_test_cfg();
7120        let dom_tree = DominatorTree::new(&cfg).unwrap();
7121
7122        let node_1 = cfg.node_indices().find(|&n| cfg[n].id == 1).unwrap();
7123
7124        // All nodes dominated by node 1 should include 1, 2, and 3
7125        let must_pass: Vec<usize> = cfg
7126            .node_indices()
7127            .filter(|&n| dom_tree.dominates(node_1, n))
7128            .map(|n| cfg[n].id)
7129            .collect();
7130
7131        assert_eq!(must_pass.len(), 3, "Block 1 should dominate 3 blocks");
7132        assert!(must_pass.contains(&1), "Must include block 1 itself");
7133        assert!(must_pass.contains(&2), "Must include block 2");
7134        assert!(must_pass.contains(&3), "Must include block 3");
7135    }
7136
7137    /// Test that non-existent block ID is handled gracefully
7138    #[test]
7139    fn test_nonexistent_block_id() {
7140        let cfg = cmds::create_test_cfg();
7141
7142        // Block ID 99 doesn't exist in test CFG
7143        let found = cfg.node_indices().find(|&n| cfg[n].id == 99);
7144        assert!(found.is_none(), "Non-existent block should not be found");
7145    }
7146
7147    /// Test JSON output for dominators command structure
7148    #[test]
7149    fn test_dominators_json_structure() {
7150        use crate::output::JsonResponse;
7151
7152        let response = DominanceResponse {
7153            function: "json_test".to_string(),
7154            kind: "post-dominators".to_string(),
7155            root: Some(3),
7156            dominance_tree: vec![DominatorEntry {
7157                block: 3,
7158                immediate_dominator: None,
7159                dominated: vec![0, 2],
7160            }],
7161            must_pass_through: Some(MustPassThroughResult {
7162                block: 0,
7163                must_pass: vec![0, 1],
7164            }),
7165        };
7166
7167        let wrapper = JsonResponse::new(response);
7168        let json = wrapper.to_json();
7169
7170        assert!(json.contains("\"kind\":\"post-dominators\""));
7171        assert!(json.contains("\"root\":3"));
7172        assert!(json.contains("\"must_pass_through\""));
7173        assert!(json.contains("\"block\":0"));
7174    }
7175}
7176
7177// ============================================================================
7178// verify() Command Tests
7179// ============================================================================
7180
7181#[cfg(test)]
7182mod verify_tests {
7183    use super::*;
7184    use crate::cfg::{enumerate_paths, PathLimits};
7185    use crate::output::JsonResponse;
7186    use crate::storage::MirageDb;
7187
7188    /// Create a test database with a cached path
7189    fn create_test_db_with_cached_path(
7190    ) -> anyhow::Result<(tempfile::NamedTempFile, MirageDb, String)> {
7191        use crate::storage::{
7192            REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
7193        };
7194        let file = tempfile::NamedTempFile::new()?;
7195        let mut conn = rusqlite::Connection::open(file.path())?;
7196
7197        // Create Magellan tables
7198        conn.execute(
7199            "CREATE TABLE magellan_meta (
7200                id INTEGER PRIMARY KEY CHECK (id = 1),
7201                magellan_schema_version INTEGER NOT NULL,
7202                sqlitegraph_schema_version INTEGER NOT NULL,
7203                created_at INTEGER NOT NULL
7204            )",
7205            [],
7206        )?;
7207
7208        conn.execute(
7209            "CREATE TABLE graph_entities (
7210                id INTEGER PRIMARY KEY AUTOINCREMENT,
7211                kind TEXT NOT NULL,
7212                name TEXT NOT NULL,
7213                file_path TEXT,
7214                data TEXT NOT NULL
7215            )",
7216            [],
7217        )?;
7218
7219        conn.execute(
7220            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
7221             VALUES (1, ?, ?, ?)",
7222            rusqlite::params![REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION, 0],
7223        )?;
7224
7225        // Create Mirage schema
7226        crate::storage::create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION)?;
7227
7228        // Add a test function
7229        conn.execute(
7230            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
7231            rusqlite::params!("function", "test_func", "test.rs", "{}"),
7232        )?;
7233        let function_id: i64 = conn.last_insert_rowid();
7234
7235        // Enumerate paths from test CFG and cache one
7236        let cfg = cmds::create_test_cfg();
7237        let paths = enumerate_paths(&cfg, &PathLimits::default());
7238
7239        // Store paths in database
7240        if let Some(first_path) = paths.first() {
7241            let path_id = &first_path.path_id;
7242
7243            // Insert path metadata
7244            conn.execute(
7245                "INSERT INTO cfg_paths (path_id, function_id, path_kind, entry_block, exit_block, length, created_at)
7246                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
7247                rusqlite::params![
7248                    path_id,
7249                    function_id,
7250                    "Normal",
7251                    first_path.entry as i64,
7252                    first_path.exit as i64,
7253                    first_path.len() as i64,
7254                    0,
7255                ],
7256            )?;
7257
7258            // Insert path elements
7259            for (idx, &block_id) in first_path.blocks.iter().enumerate() {
7260                conn.execute(
7261                    "INSERT INTO cfg_path_elements (path_id, sequence_order, block_id)
7262                     VALUES (?1, ?2, ?3)",
7263                    rusqlite::params![path_id, idx as i64, block_id as i64],
7264                )?;
7265            }
7266
7267            let db = MirageDb::open(file.path())?;
7268            Ok((file, db, path_id.clone()))
7269        } else {
7270            anyhow::bail!("No paths found in test CFG")
7271        }
7272    }
7273
7274    /// Test that verify() returns valid for a path that exists in current enumeration
7275    #[test]
7276    #[cfg(feature = "backend-sqlite")]
7277    fn test_verify_valid_path() {
7278        let (_file, _db, cached_path_id) = create_test_db_with_cached_path().unwrap();
7279
7280        // Create test CFG and enumerate to get current paths
7281        let cfg = cmds::create_test_cfg();
7282        let current_paths = enumerate_paths(&cfg, &PathLimits::default());
7283
7284        // Find the cached path in current enumeration
7285        let is_valid = current_paths.iter().any(|p| p.path_id == cached_path_id);
7286
7287        // Since we're using the same test CFG, the path should be valid
7288        assert!(is_valid, "Cached path should exist in current enumeration");
7289    }
7290
7291    /// Test that VerifyResult serializes correctly
7292    #[test]
7293    fn test_verify_result_serialization() {
7294        let result = VerifyResult {
7295            path_id: "test_path_123".to_string(),
7296            valid: true,
7297            found_in_cache: true,
7298            function_id: Some(1),
7299            reason: "Path found in current enumeration".to_string(),
7300            current_paths: 2,
7301        };
7302
7303        let json = serde_json::to_string(&result);
7304        assert!(json.is_ok());
7305
7306        let json_str = json.unwrap();
7307        assert!(json_str.contains("\"path_id\":\"test_path_123\""));
7308        assert!(json_str.contains("\"valid\":true"));
7309        assert!(json_str.contains("\"found_in_cache\":true"));
7310        assert!(json_str.contains("\"function_id\":1"));
7311        assert!(json_str.contains("\"reason\""));
7312        assert!(json_str.contains("\"current_paths\":2"));
7313    }
7314
7315    /// Test that invalid path verification returns correct result
7316    #[test]
7317    fn test_verify_invalid_path_result() {
7318        let result = VerifyResult {
7319            path_id: "nonexistent_path".to_string(),
7320            valid: false,
7321            found_in_cache: false,
7322            function_id: None,
7323            reason: "Path not found in cache".to_string(),
7324            current_paths: 0,
7325        };
7326
7327        assert!(!result.valid);
7328        assert!(!result.found_in_cache);
7329        assert!(result.function_id.is_none());
7330        assert_eq!(result.reason, "Path not found in cache");
7331    }
7332
7333    /// Test VerifyArgs struct has expected fields
7334    #[test]
7335    fn test_verify_args_fields() {
7336        let args = VerifyArgs {
7337            path_id: "abc123".to_string(),
7338        };
7339
7340        assert_eq!(args.path_id, "abc123");
7341    }
7342
7343    /// Test that JsonResponse wrapper works with VerifyResult
7344    #[test]
7345    fn test_verify_result_json_wrapper() {
7346        let result = VerifyResult {
7347            path_id: "wrapped_path".to_string(),
7348            valid: true,
7349            found_in_cache: true,
7350            function_id: Some(42),
7351            reason: "Test reason".to_string(),
7352            current_paths: 100,
7353        };
7354
7355        let wrapper = JsonResponse::new(result);
7356
7357        assert_eq!(wrapper.schema_version, "1.0.1");
7358        assert_eq!(wrapper.tool, "mirage");
7359        assert!(!wrapper.execution_id.is_empty());
7360        assert!(!wrapper.timestamp.is_empty());
7361
7362        let json = wrapper.to_json();
7363        assert!(json.contains("\"schema_version\":\"1.0.1\""));
7364        assert!(json.contains("\"tool\":\"mirage\""));
7365        assert!(json.contains("wrapped_path"));
7366    }
7367
7368    /// Test path validity check with existing path
7369    #[test]
7370    fn test_verify_check_path_exists() {
7371        let cfg = cmds::create_test_cfg();
7372        let paths = enumerate_paths(&cfg, &PathLimits::default());
7373
7374        // Get first path ID
7375        if let Some(first_path) = paths.first() {
7376            let path_id = &first_path.path_id;
7377
7378            // Check if path exists
7379            let exists = paths.iter().any(|p| &p.path_id == path_id);
7380            assert!(exists, "Path should exist in enumeration");
7381
7382            // Verify we can find it by blocks
7383            let same_blocks = paths.iter().any(|p| p.blocks == first_path.blocks);
7384            assert!(same_blocks, "Should find path with same blocks");
7385        }
7386    }
7387
7388    /// Test that multiple paths have different IDs
7389    #[test]
7390    fn test_verify_multiple_paths_have_different_ids() {
7391        let cfg = cmds::create_test_cfg();
7392        let paths = enumerate_paths(&cfg, &PathLimits::default());
7393
7394        // Test CFG should have multiple paths (2 paths for the diamond)
7395        assert!(paths.len() >= 2, "Test CFG should have at least 2 paths");
7396
7397        // Check that all path IDs are unique
7398        let mut path_ids = std::collections::HashSet::new();
7399        for path in &paths {
7400            assert!(
7401                path_ids.insert(&path.path_id),
7402                "Path ID should be unique: {}",
7403                path.path_id
7404            );
7405        }
7406    }
7407
7408    /// Test that path not in cache returns found_in_cache: false
7409    #[test]
7410    fn test_verify_path_not_in_cache() {
7411        let result = VerifyResult {
7412            path_id: "fake_id_that_does_not_exist".to_string(),
7413            valid: false,
7414            found_in_cache: false,
7415            function_id: None,
7416            reason: "Path not found in cache".to_string(),
7417            current_paths: 0,
7418        };
7419
7420        assert!(!result.found_in_cache);
7421        assert!(!result.valid);
7422    }
7423
7424    /// Test JSON output format for verify command
7425    #[test]
7426    fn test_verify_json_output_format() {
7427        let result = VerifyResult {
7428            path_id: "json_test_path".to_string(),
7429            valid: true,
7430            found_in_cache: true,
7431            function_id: Some(123),
7432            reason: "Test".to_string(),
7433            current_paths: 5,
7434        };
7435
7436        let wrapper = JsonResponse::new(result);
7437        let json = wrapper.to_pretty_json();
7438
7439        // Pretty JSON should have newlines
7440        assert!(json.contains("\n"));
7441
7442        // Verify it can be parsed back
7443        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
7444        assert_eq!(parsed["tool"], "mirage");
7445        assert_eq!(parsed["data"]["path_id"], "json_test_path");
7446        assert_eq!(parsed["data"]["valid"], true);
7447    }
7448
7449    /// Test verify response with function_id None
7450    #[test]
7451    fn test_verify_result_without_function_id() {
7452        let result = VerifyResult {
7453            path_id: "orphan_path".to_string(),
7454            valid: false,
7455            found_in_cache: false,
7456            function_id: None,
7457            reason: "No function associated".to_string(),
7458            current_paths: 10,
7459        };
7460
7461        let json = serde_json::to_string(&result).unwrap();
7462        assert!(json.contains("\"function_id\":null"));
7463        assert!(!result.valid);
7464        assert!(!result.found_in_cache);
7465    }
7466}
7467
7468// ============================================================================
7469// Output Format Consistency Tests (06-07)
7470// ============================================================================
7471
7472#[cfg(test)]
7473mod output_format_tests {
7474    use super::*;
7475    use crate::output::JsonResponse;
7476
7477    /// Test that all response structs serialize correctly to JSON
7478    #[test]
7479    fn test_all_response_types_serialize() {
7480        // PathsResponse
7481        let paths_resp = PathsResponse {
7482            function: "test_func".to_string(),
7483            total_paths: 2,
7484            error_paths: 0,
7485            paths: vec![],
7486        };
7487        let paths_json = serde_json::to_string(&paths_resp);
7488        assert!(paths_json.is_ok(), "PathsResponse should serialize");
7489
7490        // DominanceResponse
7491        let dom_resp = DominanceResponse {
7492            function: "test_func".to_string(),
7493            kind: "dominators".to_string(),
7494            root: Some(0),
7495            dominance_tree: vec![],
7496            must_pass_through: None,
7497        };
7498        let dom_json = serde_json::to_string(&dom_resp);
7499        assert!(dom_json.is_ok(), "DominanceResponse should serialize");
7500
7501        // UnreachableResponse
7502        let unreach_resp = UnreachableResponse {
7503            uncalled_functions: None,
7504            function: "test_func".to_string(),
7505            total_functions: 1,
7506            functions_with_unreachable: 0,
7507            unreachable_count: 0,
7508            blocks: vec![],
7509        };
7510        let unreach_json = serde_json::to_string(&unreach_resp);
7511        assert!(unreach_json.is_ok(), "UnreachableResponse should serialize");
7512
7513        // VerifyResult
7514        let verify_res = VerifyResult {
7515            path_id: "test_path".to_string(),
7516            valid: true,
7517            found_in_cache: true,
7518            function_id: Some(1),
7519            reason: "Test".to_string(),
7520            current_paths: 2,
7521        };
7522        let verify_json = serde_json::to_string(&verify_res);
7523        assert!(verify_json.is_ok(), "VerifyResult should serialize");
7524    }
7525
7526    /// Test that JsonResponse wrapper works for all response types
7527    #[test]
7528    fn test_json_response_wrapper_for_all_commands() {
7529        // PathsResponse wrapped
7530        let paths_resp = PathsResponse {
7531            function: "test_func".to_string(),
7532            total_paths: 2,
7533            error_paths: 0,
7534            paths: vec![],
7535        };
7536        let paths_wrapper = JsonResponse::new(paths_resp);
7537        assert_eq!(paths_wrapper.schema_version, "1.0.1");
7538        assert_eq!(paths_wrapper.tool, "mirage");
7539        assert!(!paths_wrapper.execution_id.is_empty());
7540
7541        // DominanceResponse wrapped
7542        let dom_resp = DominanceResponse {
7543            function: "test_func".to_string(),
7544            kind: "dominators".to_string(),
7545            root: Some(0),
7546            dominance_tree: vec![],
7547            must_pass_through: None,
7548        };
7549        let dom_wrapper = JsonResponse::new(dom_resp);
7550        assert_eq!(dom_wrapper.schema_version, "1.0.1");
7551        assert_eq!(dom_wrapper.tool, "mirage");
7552
7553        // UnreachableResponse wrapped
7554        let unreach_resp = UnreachableResponse {
7555            uncalled_functions: None,
7556            function: "test_func".to_string(),
7557            total_functions: 1,
7558            functions_with_unreachable: 0,
7559            unreachable_count: 0,
7560            blocks: vec![],
7561        };
7562        let unreach_wrapper = JsonResponse::new(unreach_resp);
7563        assert_eq!(unreach_wrapper.schema_version, "1.0.1");
7564        assert_eq!(unreach_wrapper.tool, "mirage");
7565
7566        // VerifyResult wrapped
7567        let verify_res = VerifyResult {
7568            path_id: "test_path".to_string(),
7569            valid: true,
7570            found_in_cache: true,
7571            function_id: Some(1),
7572            reason: "Test".to_string(),
7573            current_paths: 2,
7574        };
7575        let verify_wrapper = JsonResponse::new(verify_res);
7576        assert_eq!(verify_wrapper.schema_version, "1.0.1");
7577        assert_eq!(verify_wrapper.tool, "mirage");
7578    }
7579
7580    /// Test that to_json() produces compact JSON
7581    #[test]
7582    fn test_json_response_compact_format() {
7583        let data = vec!["item1", "item2"];
7584        let wrapper = JsonResponse::new(data);
7585        let compact = wrapper.to_json();
7586
7587        // Compact JSON should not have unnecessary whitespace
7588        assert!(
7589            !compact.contains("\n"),
7590            "Compact JSON should not have newlines"
7591        );
7592        assert!(
7593            compact.contains("\"item1\""),
7594            "Compact JSON should contain data"
7595        );
7596    }
7597
7598    /// Test that to_pretty_json() produces formatted JSON
7599    #[test]
7600    fn test_json_response_pretty_format() {
7601        let data = vec!["item1", "item2"];
7602        let wrapper = JsonResponse::new(data);
7603        let pretty = wrapper.to_pretty_json();
7604
7605        // Pretty JSON should have newlines for formatting
7606        assert!(pretty.contains("\n"), "Pretty JSON should have newlines");
7607        assert!(pretty.contains("  "), "Pretty JSON should have indentation");
7608
7609        // Both formats should produce valid JSON with same data
7610        let compact = wrapper.to_json();
7611        let compact_val: serde_json::Value = serde_json::from_str(&compact).unwrap();
7612        let pretty_val: serde_json::Value = serde_json::from_str(&pretty).unwrap();
7613        assert_eq!(
7614            compact_val, pretty_val,
7615            "Both formats should produce same data"
7616        );
7617    }
7618
7619    /// Test that JsonResponse contains required fields
7620    #[test]
7621    fn test_json_response_required_fields() {
7622        let data = "test_data";
7623        let wrapper = JsonResponse::new(data);
7624
7625        // Check all required fields exist and have correct values
7626        assert_eq!(wrapper.schema_version, "1.0.1");
7627        assert_eq!(wrapper.tool, "mirage");
7628        assert!(!wrapper.execution_id.is_empty());
7629        assert!(!wrapper.timestamp.is_empty());
7630
7631        // Verify execution_id format (should be timestamp-processid)
7632        assert!(
7633            wrapper.execution_id.contains("-"),
7634            "execution_id should contain hyphen"
7635        );
7636
7637        // Verify timestamp is valid RFC3339 format
7638        let parsed_time = chrono::DateTime::parse_from_rfc3339(&wrapper.timestamp);
7639        assert!(parsed_time.is_ok(), "timestamp should be valid RFC3339");
7640    }
7641
7642    /// Test that format selection logic works correctly
7643    #[test]
7644    fn test_output_format_enum_matches() {
7645        // Test that all three formats are distinct
7646        assert_ne!(OutputFormat::Human, OutputFormat::Json);
7647        assert_ne!(OutputFormat::Human, OutputFormat::Pretty);
7648        assert_ne!(OutputFormat::Json, OutputFormat::Pretty);
7649
7650        // Test equality
7651        assert_eq!(OutputFormat::Human, OutputFormat::Human);
7652        assert_eq!(OutputFormat::Json, OutputFormat::Json);
7653        assert_eq!(OutputFormat::Pretty, OutputFormat::Pretty);
7654    }
7655
7656    /// Test that human format doesn't contain JSON artifacts
7657    #[test]
7658    fn test_human_output_no_json_artifacts() {
7659        // Human format should print readable text, not JSON
7660        // This test verifies the pattern: Human output uses println!, not JsonResponse
7661
7662        let function_name = "test_function";
7663        let path_count = 5;
7664
7665        // Simulate human format output
7666        let mut output = String::new();
7667        output.push_str(&format!("Function: {}\n", function_name));
7668        output.push_str(&format!("Total paths: {}\n", path_count));
7669
7670        // Human output should not contain JSON artifacts
7671        assert!(
7672            !output.contains("{"),
7673            "Human output should not contain JSON objects"
7674        );
7675        assert!(
7676            !output.contains("}"),
7677            "Human output should not contain JSON objects"
7678        );
7679        assert!(
7680            !output.contains("\""),
7681            "Human output should not contain JSON quotes"
7682        );
7683        assert!(
7684            !output.contains("schema_version"),
7685            "Human output should not contain JSON metadata"
7686        );
7687    }
7688
7689    /// Test that JSON output contains all expected metadata
7690    #[test]
7691    fn test_json_output_has_metadata() {
7692        let data = "test_data";
7693        let wrapper = JsonResponse::new(data);
7694        let json = wrapper.to_json();
7695
7696        // JSON should contain all metadata fields
7697        assert!(json.contains("\"schema_version\""));
7698        assert!(json.contains("\"execution_id\""));
7699        assert!(json.contains("\"tool\""));
7700        assert!(json.contains("\"timestamp\""));
7701        assert!(json.contains("\"data\""));
7702    }
7703
7704    /// Test error response format
7705    #[test]
7706    fn test_error_response_format() {
7707        use crate::output::JsonError;
7708
7709        let error = JsonError::new("category", "message", "CODE");
7710        assert_eq!(error.error, "category");
7711        assert_eq!(error.message, "message");
7712        assert_eq!(error.code, "CODE");
7713        assert!(error.remediation.is_none());
7714
7715        let error_with_remediation = error.with_remediation("Try X instead");
7716        assert_eq!(
7717            error_with_remediation.remediation,
7718            Some("Try X instead".to_string())
7719        );
7720
7721        // Error response should serialize
7722        let json = serde_json::to_string(&error_with_remediation);
7723        assert!(json.is_ok());
7724        let json_str = json.unwrap();
7725        assert!(json_str.contains("\"error\""));
7726        assert!(json_str.contains("\"message\""));
7727        assert!(json_str.contains("\"code\""));
7728        assert!(json_str.contains("\"remediation\""));
7729    }
7730
7731    /// Test that all CLI struct variants can be created with different output formats
7732    #[test]
7733    fn test_cli_with_different_output_formats() {
7734        let formats = vec![
7735            OutputFormat::Human,
7736            OutputFormat::Json,
7737            OutputFormat::Pretty,
7738        ];
7739
7740        for format in formats {
7741            let cli = Cli {
7742                db: Some("./test.db".to_string()),
7743                output: format,
7744                command: Some(Commands::Status(StatusArgs {})),
7745                detect_backend: false,
7746            };
7747
7748            assert_eq!(cli.output, format);
7749            assert_eq!(cli.db, Some("./test.db".to_string()));
7750        }
7751    }
7752
7753    /// Test CfgFormat enum values
7754    #[test]
7755    fn test_cfg_format_enum() {
7756        let formats = vec![CfgFormat::Human, CfgFormat::Dot, CfgFormat::Json];
7757
7758        for format in &formats {
7759            match format {
7760                CfgFormat::Human => assert!(true),
7761                CfgFormat::Dot => assert!(true),
7762                CfgFormat::Json => assert!(true),
7763            }
7764        }
7765
7766        // Test distinctness
7767        assert_ne!(CfgFormat::Human, CfgFormat::Dot);
7768        assert_ne!(CfgFormat::Human, CfgFormat::Json);
7769        assert_ne!(CfgFormat::Dot, CfgFormat::Json);
7770    }
7771
7772    /// Test that response field naming follows snake_case convention
7773    #[test]
7774    fn test_response_snake_case_naming() {
7775        // All JSON field names should use snake_case
7776        let paths_resp = PathsResponse {
7777            function: "test".to_string(),
7778            total_paths: 1,
7779            error_paths: 0,
7780            paths: vec![],
7781        };
7782        let json = serde_json::to_string(&paths_resp).unwrap();
7783
7784        // Check for snake_case fields
7785        assert!(json.contains("\"function\""));
7786        assert!(json.contains("\"total_paths\""));
7787        assert!(json.contains("\"error_paths\""));
7788
7789        // Should not have camelCase
7790        assert!(!json.contains("\"totalPaths\""));
7791        assert!(!json.contains("\"errorPaths\""));
7792    }
7793
7794    /// Test loops command detects natural loops
7795    #[test]
7796    fn test_loops_detects_loops() {
7797        use crate::cfg::{detect_natural_loops, BasicBlock, BlockKind, EdgeType, Terminator};
7798        use petgraph::graph::DiGraph;
7799
7800        // Create a simple loop: 0 -> 1 -> 2 -> 1
7801        let mut g = DiGraph::new();
7802
7803        let b0 = g.add_node(BasicBlock {
7804            id: 0,
7805            db_id: None,
7806            kind: BlockKind::Entry,
7807            statements: vec![],
7808            terminator: Terminator::Goto { target: 1 },
7809            source_location: None,
7810            coord_x: 0,
7811            coord_y: 0,
7812            coord_z: 0,
7813        });
7814
7815        let b1 = g.add_node(BasicBlock {
7816            id: 1,
7817            db_id: None,
7818            kind: BlockKind::Normal,
7819            statements: vec![],
7820            terminator: Terminator::SwitchInt {
7821                targets: vec![2],
7822                otherwise: 3,
7823            },
7824            source_location: None,
7825            coord_x: 0,
7826            coord_y: 0,
7827            coord_z: 0,
7828        });
7829
7830        let b2 = g.add_node(BasicBlock {
7831            id: 2,
7832            db_id: None,
7833            kind: BlockKind::Normal,
7834            statements: vec!["loop body".to_string()],
7835            terminator: Terminator::Goto { target: 1 },
7836            source_location: None,
7837            coord_x: 0,
7838            coord_y: 0,
7839            coord_z: 0,
7840        });
7841
7842        let b3 = g.add_node(BasicBlock {
7843            id: 3,
7844            db_id: None,
7845            kind: BlockKind::Exit,
7846            statements: vec![],
7847            terminator: Terminator::Return,
7848            source_location: None,
7849            coord_x: 0,
7850            coord_y: 0,
7851            coord_z: 0,
7852        });
7853
7854        g.add_edge(b0, b1, EdgeType::Fallthrough);
7855        g.add_edge(b1, b2, EdgeType::TrueBranch);
7856        g.add_edge(b1, b3, EdgeType::FalseBranch);
7857        g.add_edge(b2, b1, EdgeType::LoopBack);
7858
7859        let loops = detect_natural_loops(&g);
7860
7861        // Should detect one loop
7862        assert_eq!(loops.len(), 1, "Should detect exactly one loop");
7863        assert_eq!(loops[0].header.index(), 1, "Loop header should be block 1");
7864    }
7865
7866    /// Test loops command with empty CFG
7867    #[test]
7868    fn test_loops_empty_cfg() {
7869        use crate::cfg::detect_natural_loops;
7870        use petgraph::graph::DiGraph;
7871        let empty_cfg: crate::cfg::Cfg = DiGraph::new();
7872        let loops = detect_natural_loops(&empty_cfg);
7873
7874        assert!(loops.is_empty(), "Empty CFG should have no loops");
7875    }
7876
7877    /// Test loops response serialization
7878    #[test]
7879    fn test_loops_response_serialization() {
7880        use crate::output::JsonResponse;
7881
7882        let response = LoopsResponse {
7883            function: "test_func".to_string(),
7884            loop_count: 2,
7885            loops: vec![
7886                LoopInfo {
7887                    header: 1,
7888                    back_edge_from: 2,
7889                    body_size: 2,
7890                    nesting_level: 0,
7891                    body_blocks: vec![1, 2],
7892                },
7893                LoopInfo {
7894                    header: 3,
7895                    back_edge_from: 4,
7896                    body_size: 3,
7897                    nesting_level: 1,
7898                    body_blocks: vec![1, 2, 3],
7899                },
7900            ],
7901        };
7902
7903        // Should serialize without errors
7904        let json = serde_json::to_string(&response).unwrap();
7905        assert!(json.contains("\"function\""));
7906        assert!(json.contains("\"loop_count\""));
7907        assert!(json.contains("\"loops\""));
7908
7909        // Test with JsonResponse wrapper
7910        let wrapper = JsonResponse::new(response);
7911        let wrapped_json = wrapper.to_json();
7912        assert!(wrapped_json.contains("\"schema_version\""));
7913        assert!(wrapped_json.contains("\"execution_id\""));
7914    }
7915
7916    /// Test LoopsArgs struct fields
7917    #[test]
7918    fn test_loops_args_fields() {
7919        let args = LoopsArgs {
7920            function: "my_function".to_string(),
7921            file: None,
7922            verbose: true,
7923        };
7924
7925        assert_eq!(args.function, "my_function");
7926        assert!(args.verbose);
7927    }
7928
7929    /// Test LoopInfo struct fields
7930    #[test]
7931    fn test_loop_info_fields() {
7932        let loop_info = LoopInfo {
7933            header: 5,
7934            back_edge_from: 7,
7935            body_size: 3,
7936            nesting_level: 2,
7937            body_blocks: vec![5, 6, 7],
7938        };
7939
7940        assert_eq!(loop_info.header, 5);
7941        assert_eq!(loop_info.back_edge_from, 7);
7942        assert_eq!(loop_info.body_size, 3);
7943        assert_eq!(loop_info.nesting_level, 2);
7944        assert_eq!(loop_info.body_blocks, vec![5, 6, 7]);
7945    }
7946
7947    /// Test loops command with json output format
7948    #[test]
7949    fn test_loops_json_output_format() {
7950        use crate::output::JsonResponse;
7951
7952        let response = LoopsResponse {
7953            function: "json_test".to_string(),
7954            loop_count: 1,
7955            loops: vec![LoopInfo {
7956                header: 1,
7957                back_edge_from: 2,
7958                body_size: 2,
7959                nesting_level: 0,
7960                body_blocks: vec![1, 2],
7961            }],
7962        };
7963
7964        let wrapper = JsonResponse::new(response);
7965        let json = wrapper.to_json();
7966
7967        // Verify JSON structure
7968        assert!(json.contains("\"schema_version\""));
7969        assert!(json.contains("\"execution_id\""));
7970        assert!(json.contains("\"tool\""));
7971        assert!(json.contains("\"timestamp\""));
7972        assert!(json.contains("\"data\""));
7973    }
7974
7975    /// Test loops command with verbose flag
7976    #[test]
7977    fn test_loops_verbose_flag() {
7978        let args_verbose = LoopsArgs {
7979            function: "test".to_string(),
7980            file: None,
7981            verbose: true,
7982        };
7983
7984        let args_not_verbose = LoopsArgs {
7985            function: "test".to_string(),
7986            file: None,
7987            verbose: false,
7988        };
7989
7990        assert!(args_verbose.verbose);
7991        assert!(!args_not_verbose.verbose);
7992    }
7993
7994    /// Test loops nesting level calculation
7995    #[test]
7996    fn test_loops_nesting_levels() {
7997        let loop_outer = LoopInfo {
7998            header: 1,
7999            back_edge_from: 3,
8000            body_size: 3,
8001            nesting_level: 0, // Outermost
8002            body_blocks: vec![1, 2, 3],
8003        };
8004
8005        let loop_inner = LoopInfo {
8006            header: 2,
8007            back_edge_from: 4,
8008            body_size: 2,
8009            nesting_level: 1, // Nested inside outer
8010            body_blocks: vec![2, 4],
8011        };
8012
8013        assert_eq!(loop_outer.nesting_level, 0);
8014        assert_eq!(loop_inner.nesting_level, 1);
8015    }
8016
8017    /// Test loops response with no loops
8018    #[test]
8019    fn test_loops_response_empty() {
8020        use crate::output::JsonResponse;
8021
8022        let response = LoopsResponse {
8023            function: "no_loops_func".to_string(),
8024            loop_count: 0,
8025            loops: vec![],
8026        };
8027
8028        let wrapper = JsonResponse::new(response);
8029        let json = wrapper.to_json();
8030
8031        // Should handle empty loops gracefully
8032        assert!(json.contains("\"loop_count\":0"));
8033        assert!(json.contains("\"loops\":[]"));
8034    }
8035
8036    /// Test patterns command with if/else detection
8037    #[test]
8038    fn test_patterns_if_else_detection() {
8039        use crate::cfg::{detect_if_else_patterns, detect_match_patterns};
8040
8041        let cfg = cmds::create_test_cfg();
8042
8043        // Detect patterns
8044        let if_else_patterns = detect_if_else_patterns(&cfg);
8045        let match_patterns = detect_match_patterns(&cfg);
8046
8047        // Test CFG has a simple if/else (block 1 -> blocks 2 and 3)
8048        // This is a diamond pattern, so it should be detected
8049        assert!(
8050            !if_else_patterns.is_empty(),
8051            "Should detect if/else pattern"
8052        );
8053
8054        // Check pattern structure
8055        let pattern = &if_else_patterns[0];
8056        assert_eq!(cfg[pattern.condition].id, 1);
8057        assert_eq!(cfg[pattern.true_branch].id, 2);
8058        assert_eq!(cfg[pattern.false_branch].id, 3);
8059
8060        // Our test CFG doesn't have a match statement
8061        assert!(
8062            match_patterns.is_empty(),
8063            "Should not detect match patterns in simple if/else"
8064        );
8065    }
8066
8067    /// Test patterns command with --if-else filter
8068    #[test]
8069    fn test_patterns_if_else_filter() {
8070        // Test argument parsing - command structure is correct
8071        let args = PatternsArgs {
8072            function: "test_func".to_string(),
8073            file: None,
8074            if_else: true,
8075            r#match: false,
8076        };
8077
8078        // Verify args are parsed correctly
8079        assert!(args.if_else);
8080        assert!(!args.r#match);
8081        assert_eq!(args.function, "test_func");
8082    }
8083
8084    /// Test patterns command with --match filter
8085    #[test]
8086    fn test_patterns_match_filter() {
8087        // Test argument parsing - command structure is correct
8088        let args = PatternsArgs {
8089            function: "test_func".to_string(),
8090            file: None,
8091            if_else: false,
8092            r#match: true,
8093        };
8094
8095        // Verify args are parsed correctly
8096        assert!(!args.if_else);
8097        assert!(args.r#match);
8098        assert_eq!(args.function, "test_func");
8099    }
8100
8101    /// Test patterns command with JSON output
8102    #[test]
8103    fn test_patterns_json_output() {
8104        // Test argument parsing - command structure is correct
8105        let args = PatternsArgs {
8106            function: "test_func".to_string(),
8107            file: None,
8108            if_else: false,
8109            r#match: false,
8110        };
8111
8112        let cli = Cli {
8113            db: None,
8114            output: OutputFormat::Json,
8115            command: Some(Commands::Patterns(args.clone())),
8116            detect_backend: false,
8117        };
8118
8119        // Verify CLI structure
8120        assert!(matches!(cli.output, OutputFormat::Json));
8121    }
8122
8123    /// Test patterns response struct serialization
8124    #[test]
8125    fn test_patterns_response_serialization() {
8126        let response = PatternsResponse {
8127            function: "test_func".to_string(),
8128            if_else_count: 1,
8129            match_count: 0,
8130            if_else_patterns: vec![IfElseInfo {
8131                condition_block: 1,
8132                true_branch: 2,
8133                false_branch: 3,
8134                merge_point: Some(4),
8135                has_else: true,
8136            }],
8137            match_patterns: vec![],
8138        };
8139
8140        // Should serialize to JSON
8141        let json = serde_json::to_string(&response).unwrap();
8142        assert!(json.contains("\"function\""));
8143        assert!(json.contains("\"if_else_count\""));
8144        assert!(json.contains("\"match_count\""));
8145
8146        // Check snake_case naming
8147        assert!(json.contains("\"if_else_patterns\""));
8148        assert!(json.contains("\"condition_block\""));
8149        assert!(json.contains("\"merge_point\""));
8150    }
8151}
8152
8153// ============================================================================
8154// frontiers() Command Tests
8155// ============================================================================
8156
8157#[cfg(test)]
8158mod frontiers_tests {
8159    use super::*;
8160    use crate::cfg::{compute_dominance_frontiers, DominatorTree};
8161    use tempfile::NamedTempFile;
8162
8163    /// Create a minimal test database
8164    fn create_minimal_db() -> anyhow::Result<NamedTempFile> {
8165        use crate::storage::{
8166            REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION,
8167        };
8168        let file = NamedTempFile::new()?;
8169        let conn = rusqlite::Connection::open(file.path())?;
8170
8171        // Create Magellan tables
8172        conn.execute(
8173            "CREATE TABLE magellan_meta (
8174                id INTEGER PRIMARY KEY CHECK (id = 1),
8175                magellan_schema_version INTEGER NOT NULL,
8176                sqlitegraph_schema_version INTEGER NOT NULL,
8177                created_at INTEGER NOT NULL
8178            )",
8179            [],
8180        )?;
8181
8182        conn.execute(
8183            "CREATE TABLE graph_entities (
8184                id INTEGER PRIMARY KEY,
8185                type TEXT NOT NULL,
8186                name TEXT,
8187                source_file TEXT
8188            )",
8189            [],
8190        )?;
8191
8192        conn.execute(
8193            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
8194             VALUES (1, ?, ?, strftime('%s', 'now'))",
8195            [REQUIRED_MAGELLAN_SCHEMA_VERSION, REQUIRED_SQLITEGRAPH_SCHEMA_VERSION],
8196        )?;
8197
8198        Ok(file)
8199    }
8200
8201    /// Test frontiers response struct serialization
8202    #[test]
8203    fn test_frontiers_response_serialization() {
8204        use crate::output::JsonResponse;
8205
8206        let response = FrontiersResponse {
8207            function: "test_func".to_string(),
8208            nodes_with_frontiers: 2,
8209            frontiers: vec![
8210                NodeFrontier {
8211                    node: 1,
8212                    frontier_set: vec![3],
8213                },
8214                NodeFrontier {
8215                    node: 2,
8216                    frontier_set: vec![3],
8217                },
8218            ],
8219        };
8220
8221        let wrapper = JsonResponse::new(response);
8222        let json = wrapper.to_json();
8223
8224        // Verify JSON structure
8225        assert!(json.contains("\"function\":\"test_func\""));
8226        assert!(json.contains("\"nodes_with_frontiers\":2"));
8227        assert!(json.contains("\"frontiers\":["));
8228    }
8229
8230    /// Test iterated frontier response struct serialization
8231    #[test]
8232    fn test_iterated_frontier_response_serialization() {
8233        use crate::output::JsonResponse;
8234
8235        let response = IteratedFrontierResponse {
8236            function: "test_func".to_string(),
8237            iterated_frontier: vec![3, 4],
8238        };
8239
8240        let wrapper = JsonResponse::new(response);
8241        let json = wrapper.to_json();
8242
8243        // Verify JSON structure
8244        assert!(json.contains("\"function\":\"test_func\""));
8245        assert!(json.contains("\"iterated_frontier\":[3,4]"));
8246    }
8247
8248    /// Test basic frontier computation (diamond CFG)
8249    #[test]
8250    fn test_frontiers_basic() {
8251        use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8252        use petgraph::graph::DiGraph;
8253
8254        // Create diamond CFG: 0 -> 1,2 -> 3
8255        let mut g = DiGraph::new();
8256
8257        let b0 = g.add_node(BasicBlock {
8258            id: 0,
8259            db_id: None,
8260            kind: BlockKind::Entry,
8261            statements: vec![],
8262            terminator: Terminator::SwitchInt {
8263                targets: vec![1],
8264                otherwise: 2,
8265            },
8266            source_location: None,
8267            coord_x: 0,
8268            coord_y: 0,
8269            coord_z: 0,
8270        });
8271
8272        let b1 = g.add_node(BasicBlock {
8273            id: 1,
8274            db_id: None,
8275            kind: BlockKind::Normal,
8276            statements: vec!["branch 1".to_string()],
8277            terminator: Terminator::Goto { target: 3 },
8278            source_location: None,
8279            coord_x: 0,
8280            coord_y: 0,
8281            coord_z: 0,
8282        });
8283
8284        let b2 = g.add_node(BasicBlock {
8285            id: 2,
8286            db_id: None,
8287            kind: BlockKind::Normal,
8288            statements: vec!["branch 2".to_string()],
8289            terminator: Terminator::Goto { target: 3 },
8290            source_location: None,
8291            coord_x: 0,
8292            coord_y: 0,
8293            coord_z: 0,
8294        });
8295
8296        let b3 = g.add_node(BasicBlock {
8297            id: 3,
8298            db_id: None,
8299            kind: BlockKind::Exit,
8300            statements: vec![],
8301            terminator: Terminator::Return,
8302            source_location: None,
8303            coord_x: 0,
8304            coord_y: 0,
8305            coord_z: 0,
8306        });
8307
8308        g.add_edge(b0, b1, EdgeType::TrueBranch);
8309        g.add_edge(b0, b2, EdgeType::FalseBranch);
8310        g.add_edge(b1, b3, EdgeType::Fallthrough);
8311        g.add_edge(b2, b3, EdgeType::Fallthrough);
8312
8313        // Compute dominance frontiers
8314        let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8315        let frontiers = compute_dominance_frontiers(&g, dom_tree);
8316
8317        // In diamond CFG:
8318        // DF[1] = {3} (1 dominates itself, pred of 3, doesn't strictly dominate 3)
8319        // DF[2] = {3} (2 dominates itself, pred of 3, doesn't strictly dominate 3)
8320        let df1 = frontiers.frontier(b1);
8321        assert!(df1.contains(&b3));
8322        assert_eq!(df1.len(), 1);
8323
8324        let df2 = frontiers.frontier(b2);
8325        assert!(df2.contains(&b3));
8326        assert_eq!(df2.len(), 1);
8327
8328        // Entry (0) has empty frontier (strictly dominates all nodes)
8329        let df0 = frontiers.frontier(b0);
8330        assert!(df0.is_empty());
8331    }
8332
8333    /// Test --iterated flag functionality
8334    #[test]
8335    fn test_frontiers_iterated_flag() {
8336        let args = FrontiersArgs {
8337            function: "test_func".to_string(),
8338            file: None,
8339            iterated: true,
8340            node: None,
8341        };
8342
8343        assert!(args.iterated);
8344        assert!(args.node.is_none());
8345    }
8346
8347    /// Test --node flag functionality
8348    #[test]
8349    fn test_frontiers_node_flag() {
8350        let args = FrontiersArgs {
8351            function: "test_func".to_string(),
8352            file: None,
8353            iterated: false,
8354            node: Some(5),
8355        };
8356
8357        assert!(!args.iterated);
8358        assert_eq!(args.node, Some(5));
8359    }
8360
8361    /// Test frontiers with linear CFG (empty frontiers)
8362    #[test]
8363    fn test_frontiers_linear_cfg() {
8364        use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8365        use petgraph::graph::DiGraph;
8366
8367        // Linear CFG: 0 -> 1 -> 2 -> 3
8368        let mut g = DiGraph::new();
8369
8370        let b0 = g.add_node(BasicBlock {
8371            id: 0,
8372            db_id: None,
8373            kind: BlockKind::Entry,
8374            statements: vec![],
8375            terminator: Terminator::Goto { target: 1 },
8376            source_location: None,
8377            coord_x: 0,
8378            coord_y: 0,
8379            coord_z: 0,
8380        });
8381
8382        let b1 = g.add_node(BasicBlock {
8383            id: 1,
8384            db_id: None,
8385            kind: BlockKind::Normal,
8386            statements: vec![],
8387            terminator: Terminator::Goto { target: 2 },
8388            source_location: None,
8389            coord_x: 0,
8390            coord_y: 0,
8391            coord_z: 0,
8392        });
8393
8394        let b2 = g.add_node(BasicBlock {
8395            id: 2,
8396            db_id: None,
8397            kind: BlockKind::Normal,
8398            statements: vec![],
8399            terminator: Terminator::Goto { target: 3 },
8400            source_location: None,
8401            coord_x: 0,
8402            coord_y: 0,
8403            coord_z: 0,
8404        });
8405
8406        let b3 = g.add_node(BasicBlock {
8407            id: 3,
8408            db_id: None,
8409            kind: BlockKind::Exit,
8410            statements: vec![],
8411            terminator: Terminator::Return,
8412            source_location: None,
8413            coord_x: 0,
8414            coord_y: 0,
8415            coord_z: 0,
8416        });
8417
8418        g.add_edge(b0, b1, EdgeType::Fallthrough);
8419        g.add_edge(b1, b2, EdgeType::Fallthrough);
8420        g.add_edge(b2, b3, EdgeType::Fallthrough);
8421
8422        // Compute dominance frontiers
8423        let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8424        let frontiers = compute_dominance_frontiers(&g, dom_tree);
8425
8426        // Linear CFG has no dominance frontiers (no join points)
8427        let nodes_with_frontiers: Vec<_> = frontiers.nodes_with_frontiers().collect();
8428        assert!(nodes_with_frontiers.is_empty());
8429    }
8430
8431    /// Test frontiers with loop CFG (self-frontier)
8432    #[test]
8433    fn test_frontiers_loop_cfg() {
8434        use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
8435        use petgraph::graph::DiGraph;
8436
8437        // Loop CFG: 0 -> 1 <-> 2 (back edge), 1 -> 3 (exit)
8438        let mut g = DiGraph::new();
8439
8440        let b0 = g.add_node(BasicBlock {
8441            id: 0,
8442            db_id: None,
8443            kind: BlockKind::Entry,
8444            statements: vec![],
8445            terminator: Terminator::Goto { target: 1 },
8446            source_location: None,
8447            coord_x: 0,
8448            coord_y: 0,
8449            coord_z: 0,
8450        });
8451
8452        let b1 = g.add_node(BasicBlock {
8453            id: 1,
8454            db_id: None,
8455            kind: BlockKind::Normal,
8456            statements: vec![],
8457            terminator: Terminator::SwitchInt {
8458                targets: vec![2],
8459                otherwise: 3,
8460            },
8461            source_location: None,
8462            coord_x: 0,
8463            coord_y: 0,
8464            coord_z: 0,
8465        });
8466
8467        let b2 = g.add_node(BasicBlock {
8468            id: 2,
8469            db_id: None,
8470            kind: BlockKind::Normal,
8471            statements: vec!["loop body".to_string()],
8472            terminator: Terminator::Goto { target: 1 },
8473            source_location: None,
8474            coord_x: 0,
8475            coord_y: 0,
8476            coord_z: 0,
8477        });
8478
8479        let b3 = g.add_node(BasicBlock {
8480            id: 3,
8481            db_id: None,
8482            kind: BlockKind::Exit,
8483            statements: vec![],
8484            terminator: Terminator::Return,
8485            source_location: None,
8486            coord_x: 0,
8487            coord_y: 0,
8488            coord_z: 0,
8489        });
8490
8491        g.add_edge(b0, b1, EdgeType::Fallthrough);
8492        g.add_edge(b1, b2, EdgeType::TrueBranch);
8493        g.add_edge(b1, b3, EdgeType::FalseBranch);
8494        g.add_edge(b2, b1, EdgeType::LoopBack);
8495
8496        // Compute dominance frontiers
8497        let dom_tree = DominatorTree::new(&g).expect("CFG has entry");
8498        let frontiers = compute_dominance_frontiers(&g, dom_tree);
8499
8500        // Loop header (1) should have self-frontier due to back edge
8501        let df1 = frontiers.frontier(b1);
8502        assert!(df1.contains(&b1), "Loop header should have self-frontier");
8503    }
8504
8505    /// Test frontiers command with json output format
8506    #[test]
8507    fn test_frontiers_json_output_format() {
8508        use crate::output::JsonResponse;
8509
8510        let response = FrontiersResponse {
8511            function: "json_test".to_string(),
8512            nodes_with_frontiers: 2,
8513            frontiers: vec![
8514                NodeFrontier {
8515                    node: 1,
8516                    frontier_set: vec![3],
8517                },
8518                NodeFrontier {
8519                    node: 2,
8520                    frontier_set: vec![3],
8521                },
8522            ],
8523        };
8524
8525        let wrapper = JsonResponse::new(response);
8526        let json = wrapper.to_json();
8527
8528        // Verify JSON structure with metadata
8529        assert!(json.contains("\"schema_version\""));
8530        assert!(json.contains("\"execution_id\""));
8531        assert!(json.contains("\"tool\""));
8532        assert!(json.contains("\"timestamp\""));
8533        assert!(json.contains("\"data\""));
8534    }
8535
8536    /// Test frontiers response with empty frontiers
8537    #[test]
8538    fn test_frontiers_response_empty() {
8539        use crate::output::JsonResponse;
8540
8541        let response = FrontiersResponse {
8542            function: "linear_func".to_string(),
8543            nodes_with_frontiers: 0,
8544            frontiers: vec![],
8545        };
8546
8547        let wrapper = JsonResponse::new(response);
8548        let json = wrapper.to_json();
8549
8550        // Should handle empty frontiers gracefully
8551        assert!(json.contains("\"nodes_with_frontiers\":0"));
8552        assert!(json.contains("\"frontiers\":[]"));
8553    }
8554
8555    // ============================================================================
8556    // Hotspots Command Tests
8557    // ============================================================================
8558
8559    /// Test hotspots args parsing
8560    #[test]
8561    fn test_hotspots_args_parsing() {
8562        let args = HotspotsArgs {
8563            entry: "main".to_string(),
8564            top: 10,
8565            min_paths: Some(5),
8566            verbose: true,
8567            inter_procedural: false,
8568            intra_procedural: false,
8569        };
8570
8571        assert_eq!(args.entry, "main");
8572        assert_eq!(args.top, 10);
8573        assert_eq!(args.min_paths, Some(5));
8574        assert!(args.verbose);
8575        assert!(!args.inter_procedural);
8576    }
8577
8578    /// Test hotspots entry point default
8579    #[test]
8580    fn test_hotspots_args_default_entry() {
8581        let args = HotspotsArgs {
8582            entry: "main".to_string(), // default value
8583            top: 20,
8584            min_paths: None,
8585            verbose: false,
8586            inter_procedural: false,
8587            intra_procedural: false,
8588        };
8589
8590        assert_eq!(args.entry, "main");
8591        assert_eq!(args.top, 20); // default value
8592    }
8593
8594    /// Test hotspot entry serialization
8595    #[test]
8596    fn test_hotspot_entry_serialization() {
8597        let entry = HotspotEntry {
8598            function: "test_func".to_string(),
8599            risk_score: 42.5,
8600            path_count: 10,
8601            dominance_factor: 1.5,
8602            complexity: 5,
8603            file_path: "test.rs".to_string(),
8604        };
8605
8606        let json = serde_json::to_string(&entry).unwrap();
8607        assert!(json.contains("test_func"));
8608        assert!(json.contains("42.5"));
8609        assert!(json.contains("\"path_count\":10"));
8610    }
8611
8612    /// Test hotspots response serialization
8613    #[test]
8614    fn test_hotspots_response_serialization() {
8615        use crate::output::JsonResponse;
8616
8617        let response = HotspotsResponse {
8618            entry_point: "main".to_string(),
8619            total_functions: 100,
8620            hotspots: vec![],
8621            mode: "intra-procedural".to_string(),
8622        };
8623
8624        let wrapper = JsonResponse::new(response);
8625        let json = wrapper.to_json();
8626
8627        assert!(json.contains("\"entry_point\":\"main\""));
8628        assert!(json.contains("\"total_functions\":100"));
8629        assert!(json.contains("intra-procedural"));
8630    }
8631
8632    /// Test hotspots response with entries
8633    #[test]
8634    fn test_hotspots_response_with_entries() {
8635        use crate::output::JsonResponse;
8636
8637        let hotspot = HotspotEntry {
8638            function: "risky_func".to_string(),
8639            risk_score: 85.0,
8640            path_count: 50,
8641            dominance_factor: 3.0,
8642            complexity: 15,
8643            file_path: "src/lib.rs".to_string(),
8644        };
8645
8646        let response = HotspotsResponse {
8647            entry_point: "main".to_string(),
8648            total_functions: 10,
8649            hotspots: vec![hotspot],
8650            mode: "inter-procedural".to_string(),
8651        };
8652
8653        let wrapper = JsonResponse::new(response);
8654        let json = wrapper.to_json();
8655
8656        assert!(json.contains("risky_func"));
8657        assert!(json.contains("85"));
8658        assert!(json.contains("inter-procedural"));
8659    }
8660
8661    /// Test hotspots clone (needed for vector operations)
8662    #[test]
8663    fn test_hotspot_entry_clone() {
8664        let entry = HotspotEntry {
8665            function: "func".to_string(),
8666            risk_score: 1.0,
8667            path_count: 1,
8668            dominance_factor: 1.0,
8669            complexity: 1,
8670            file_path: "file.rs".to_string(),
8671        };
8672
8673        let cloned = entry.clone();
8674        assert_eq!(entry.function, cloned.function);
8675        assert_eq!(entry.risk_score, cloned.risk_score);
8676    }
8677
8678    // ============================================================================
8679    // Hotpaths Command Tests
8680    // ============================================================================
8681
8682    /// Test hotpaths args parsing
8683    #[test]
8684    fn test_hotpaths_args_parsing() {
8685        let args = HotpathsArgs {
8686            function: "my_function".to_string(),
8687            top: 5,
8688            rationale: true,
8689            min_score: Some(0.5),
8690        };
8691
8692        assert_eq!(args.function, "my_function");
8693        assert_eq!(args.top, 5);
8694        assert!(args.rationale);
8695        assert_eq!(args.min_score, Some(0.5));
8696    }
8697
8698    /// Test hotpaths args defaults
8699    #[test]
8700    fn test_hotpaths_args_defaults() {
8701        let args = HotpathsArgs {
8702            function: "main".to_string(),
8703            top: 10, // default value
8704            rationale: false,
8705            min_score: None,
8706        };
8707
8708        assert_eq!(args.function, "main");
8709        assert_eq!(args.top, 10); // default value
8710        assert!(!args.rationale);
8711        assert!(args.min_score.is_none());
8712    }
8713
8714    // ============================================================================
8715    // Inter-Procedural Dominance Tests
8716    // ============================================================================
8717
8718    /// Test dominators args has inter_procedural flag
8719    #[test]
8720    fn test_dominators_args_has_inter_procedural_flag() {
8721        let args = DominatorsArgs {
8722            function: "main".to_string(),
8723            file: None,
8724            must_pass_through: Some("block1".to_string()),
8725            post: false,
8726            inter_procedural: true,
8727        };
8728
8729        assert!(args.inter_procedural);
8730        assert_eq!(args.function, "main");
8731        assert_eq!(args.must_pass_through, Some("block1".to_string()));
8732        assert!(!args.post);
8733    }
8734
8735    /// Test dominators args without inter_procedural flag
8736    #[test]
8737    fn test_dominators_args_default_intra_procedural() {
8738        let args = DominatorsArgs {
8739            function: "main".to_string(),
8740            file: None,
8741            must_pass_through: None,
8742            post: false,
8743            inter_procedural: false, // default
8744        };
8745
8746        assert!(!args.inter_procedural);
8747        assert!(!args.post);
8748        assert!(args.must_pass_through.is_none());
8749    }
8750
8751    /// Test inter-procedural dominance with post flag combination
8752    #[test]
8753    fn test_dominators_inter_procedural_with_post() {
8754        // In practice, inter_procedural mode should take precedence
8755        let args = DominatorsArgs {
8756            function: "entry".to_string(),
8757            file: None,
8758            must_pass_through: None,
8759            post: true,
8760            inter_procedural: true,
8761        };
8762
8763        // Both flags can be set (inter_procedural takes precedence in handler)
8764        assert!(args.inter_procedural);
8765        assert!(args.post);
8766    }
8767
8768    /// Test inter-procedural mode cannot use must_pass_through
8769    #[test]
8770    fn test_dominators_inter_procedural_must_pass_through_combination() {
8771        // These flags can coexist in args struct
8772        let args = DominatorsArgs {
8773            function: "main".to_string(),
8774            file: None,
8775            must_pass_through: Some("some_block".to_string()),
8776            post: false,
8777            inter_procedural: true,
8778        };
8779
8780        assert!(args.inter_procedural);
8781        assert!(args.must_pass_through.is_some());
8782        // Handler should validate this combination
8783    }
8784}