Skip to main content

sqry_cli/commands/
repair.rs

1//! Index repair command implementation
2//!
3//! Provides tools to detect and fix common index issues:
4//! - Orphaned symbols (files no longer exist)
5//! - Dangling references (symbols reference non-existent dependencies)
6//!
7//! Note: Repair now uses the unified graph for detection and recommends
8//! rebuilding the index for fixes.
9
10use crate::args::Cli;
11use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph};
12use crate::index_discovery::find_nearest_index;
13use anyhow::{Context, Result};
14use std::path::Path;
15
16/// Repair corrupted index by detecting issues and recommending fixes
17///
18/// # Arguments
19///
20/// * `cli` - CLI configuration
21/// * `path` - Directory with existing index
22/// * `fix_orphans` - Remove symbols for files that no longer exist
23/// * `fix_dangling` - Remove dangling references
24/// * `recompute_checksum` - Recompute index checksum
25/// * `fix_all` - Apply all fixes
26/// * `dry_run` - Preview changes without modifying
27///
28/// # Errors
29/// Returns an error if the graph cannot be loaded.
30#[allow(clippy::fn_params_excessive_bools)]
31pub fn run_repair(
32    cli: &Cli,
33    path: &str,
34    fix_orphans: bool,
35    fix_dangling: bool,
36    recompute_checksum: bool,
37    fix_all: bool,
38    dry_run: bool,
39) -> Result<()> {
40    let root_path = Path::new(path);
41
42    // Find index
43    let index_location = find_nearest_index(root_path);
44    let Some(ref loc) = index_location else {
45        anyhow::bail!(
46            "No index found at {}. Run 'sqry index' first.",
47            root_path.display()
48        );
49    };
50
51    // Load unified graph
52    let config = GraphLoadConfig::default();
53    let graph = load_unified_graph(&loc.index_root, &config)
54        .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
55
56    let Some(actions) =
57        determine_repair_actions(fix_all, fix_orphans, fix_dangling, recompute_checksum)
58    else {
59        return Ok(());
60    };
61
62    if dry_run {
63        println!("DRY RUN MODE - Detecting issues only\n");
64    }
65
66    let mut stats = RepairStats::default();
67    let mut issues_found = false;
68
69    // Check for orphaned files
70    if actions.orphans {
71        println!("Checking for orphaned symbols...");
72        let orphan_count = detect_orphaned_nodes(&graph, &loc.index_root);
73        stats.orphans_detected = orphan_count;
74        if orphan_count > 0 {
75            println!("  Found {orphan_count} orphaned files (missing from disk)");
76            issues_found = true;
77        } else {
78            println!("  No orphaned files found");
79        }
80    }
81
82    if actions.dangling {
83        println!("Checking for dangling references...");
84        println!("  Note: Dangling reference detection requires full graph analysis");
85    }
86
87    if actions.checksum {
88        println!("Checksum verification...");
89        println!("  Graph integrity verified during load");
90        stats.checksum_verified = true;
91    }
92
93    // Report results
94    if issues_found {
95        println!("\n⚠ Issues detected. To fix, rebuild the index:");
96        println!("  sqry index --force {}", root_path.display());
97    } else {
98        println!("\n✓ No issues detected - index is healthy");
99    }
100
101    if cli.json {
102        print_repair_json(issues_found, dry_run, &stats)?;
103    }
104
105    Ok(())
106}
107
108struct RepairActions {
109    orphans: bool,
110    dangling: bool,
111    checksum: bool,
112}
113
114#[allow(clippy::fn_params_excessive_bools)]
115fn determine_repair_actions(
116    fix_all: bool,
117    fix_orphans: bool,
118    fix_dangling: bool,
119    recompute_checksum: bool,
120) -> Option<RepairActions> {
121    let actions = RepairActions {
122        orphans: fix_all || fix_orphans,
123        dangling: fix_all || fix_dangling,
124        checksum: fix_all || recompute_checksum,
125    };
126
127    if !actions.orphans && !actions.dangling && !actions.checksum {
128        report_missing_repair_options();
129        return None;
130    }
131
132    Some(actions)
133}
134
135fn report_missing_repair_options() {
136    eprintln!("No repair options specified. Use --fix-all or specify individual checks:");
137    eprintln!("  --fix-orphans           Check for symbols with missing files");
138    eprintln!("  --fix-dangling          Check for dangling references");
139    eprintln!("  --recompute-checksum    Verify index integrity");
140    eprintln!("  --fix-all               Run all checks");
141}
142
143/// Detect nodes whose files no longer exist on disk
144fn detect_orphaned_nodes(
145    graph: &sqry_core::graph::unified::concurrent::CodeGraph,
146    root_path: &Path,
147) -> usize {
148    let files = graph.files();
149    let mut orphan_count = 0;
150
151    // Check each unique file in the graph
152    for (node_id, entry) in graph.nodes().iter() {
153        let _ = node_id; // Suppress unused warning
154
155        if let Some(file_path) = files.resolve(entry.file) {
156            let full_path = root_path.join(file_path.as_ref());
157            if !full_path.exists() {
158                orphan_count += 1;
159            }
160        }
161    }
162
163    orphan_count
164}
165
166fn print_repair_json(issues_found: bool, dry_run: bool, stats: &RepairStats) -> Result<()> {
167    let json_output = serde_json::json!({
168        "issues_found": issues_found,
169        "dry_run": dry_run,
170        "stats": {
171            "orphans_detected": stats.orphans_detected,
172            "dangling_refs_detected": stats.dangling_refs_detected,
173            "checksum_verified": stats.checksum_verified,
174        },
175        "recommendation": if issues_found { "Run 'sqry index --force' to rebuild" } else { "No action needed" }
176    });
177    println!("{}", serde_json::to_string_pretty(&json_output)?);
178    Ok(())
179}
180
181/// Statistics about issues detected
182#[derive(Default)]
183struct RepairStats {
184    orphans_detected: usize,
185    dangling_refs_detected: usize,
186    checksum_verified: bool,
187}