Skip to main content

microscope_memory/
doctor.rs

1//! doctor.rs — Integrity diagnostics and automatic repair for Microscope Memory.
2//! (v0.7.0 Public Beta - Reliability Phase)
3
4use crate::config::Config;
5use crate::merkle::MerkleTree;
6use crate::reader::MicroscopeReader;
7use colored::Colorize;
8use std::fs;
9use std::path::Path;
10
11pub fn run_doctor(config: &Config, fix: bool) -> Result<(), String> {
12    println!("{}", "=".repeat(60));
13    println!(
14        "  {}",
15        "MICROSCOPE MEMORY DOCTOR — Integrity Scan".cyan().bold()
16    );
17    println!("{}", "=".repeat(60));
18
19    let output_dir = Path::new(&config.paths.output_dir);
20    let mut artifacts_missing = false;
21
22    // 1. Check core artifacts
23    let core_files = ["meta.bin", "microscope.bin", "data.bin"];
24    for file in &core_files {
25        let p = output_dir.join(file);
26        if !p.exists() {
27            println!("  [{}] {} is missing", "ERR".red(), file);
28            artifacts_missing = true;
29        } else {
30            println!(
31                "  [{}] {} present ({:.1} KB)",
32                "OK".green(),
33                file,
34                fs::metadata(&p)
35                    .map(|m| m.len() as f64 / 1024.0)
36                    .unwrap_or(0.0)
37            );
38        }
39    }
40
41    if artifacts_missing {
42        println!(
43            "\n  {} Core artifacts are missing. Run 'build' to regenerate.",
44            "WARN:".yellow()
45        );
46        return Ok(());
47    }
48
49    // 2. Comprehensive Integrity Scan
50    let reader = MicroscopeReader::open(config)?;
51    println!(
52        "\n  {} scanning {} blocks...",
53        "INTEGRITY:".yellow(),
54        reader.block_count
55    );
56
57    let mut bad_crc = 0;
58    for i in 0..reader.block_count {
59        let h = reader.header(i);
60        let stored_crc = u16::from_le_bytes(h.crc16);
61        if stored_crc == 0 {
62            continue;
63        }
64
65        let start = h.data_offset as usize;
66        let end = start + h.data_len as usize;
67        if end > reader.data.len() {
68            bad_crc += 1;
69            continue;
70        }
71
72        let computed = crate::crc16_ccitt(&reader.data[start..end]);
73        if computed != stored_crc {
74            bad_crc += 1;
75        }
76    }
77
78    if bad_crc == 0 {
79        println!("  [{}] All block CRCs verified.", "OK".green());
80    } else {
81        println!(
82            "  [{}] {} block(s) have CRC mismatches!",
83            "FAIL".red(),
84            bad_crc
85        );
86    }
87
88    // 3. Merkle Root Check
89    let merkle_path = output_dir.join("merkle.bin");
90    if merkle_path.exists() {
91        if let Ok(merkle_data) = fs::read(&merkle_path) {
92            if let Some(tree) = MerkleTree::from_bytes(&merkle_data) {
93                println!(
94                    "  [{}] Merkle tree loaded (Root: {})",
95                    "OK".green(),
96                    crate::hex_str(&tree.root)
97                );
98            }
99        }
100    }
101
102    // 4. Append Log Health & Repair
103    println!("\n  {} checking append log...", "RECOVERY:".yellow());
104    let append_path = output_dir.join("append.bin");
105    if append_path.exists() {
106        let data = fs::read(&append_path).map_err(|e| e.to_string())?;
107        if data.is_empty() {
108            println!("  [{}] Append log is empty.", "OK".green());
109        } else {
110            let mut pos = 0;
111            let (is_v2, is_v3) = if data.len() >= 4 {
112                (&data[0..4] == b"APv2", &data[0..4] == b"APv3")
113            } else {
114                (false, false)
115            };
116            if is_v2 || is_v3 {
117                pos = 4;
118            }
119            let header_size = if is_v3 {
120                35
121            } else if is_v2 {
122                19
123            } else {
124                18
125            };
126
127            let mut valid_pos = pos;
128            let mut count = 0;
129            let mut corrupted = false;
130
131            while pos + header_size <= data.len() {
132                let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
133                if pos + header_size + len > data.len() {
134                    corrupted = true;
135                    break;
136                }
137                pos += header_size + len;
138                valid_pos = pos;
139                count += 1;
140            }
141
142            if pos < data.len() {
143                corrupted = true;
144            }
145
146            if corrupted {
147                println!(
148                    "  [{}] Found corrupted data at tail. Valid entries: {}",
149                    "WARN".yellow(),
150                    count
151                );
152                if fix {
153                    println!(
154                        "  [{}] Truncating append.bin to last valid position ({} bytes)...",
155                        "FIX".green(),
156                        valid_pos
157                    );
158                    let f = fs::OpenOptions::new()
159                        .write(true)
160                        .open(&append_path)
161                        .map_err(|e| e.to_string())?;
162                    f.set_len(valid_pos as u64).map_err(|e| e.to_string())?;
163                    // Append vectors are keyed by position in the log we just
164                    // truncated, so they must go with the tail that was lost --
165                    // otherwise a vector would answer for a different memory.
166                    let sidecar = output_dir.join(crate::embedding_index::APPEND_EMBEDDINGS_FILE);
167                    if sidecar.exists() {
168                        fs::remove_file(&sidecar).map_err(|e| e.to_string())?;
169                        println!(
170                            "  [{}] Removed {} (its positions no longer exist).",
171                            "DONE".green(),
172                            crate::embedding_index::APPEND_EMBEDDINGS_FILE
173                        );
174                    }
175                    println!("  [{}] Truncation successful.", "DONE".green());
176                } else {
177                    println!(
178                        "  [{}] Run with --fix to truncate and recover the log.",
179                        "INFO".cyan()
180                    );
181                }
182            } else {
183                println!(
184                    "  [{}] Append log is healthy. Entries: {}",
185                    "OK".green(),
186                    count
187                );
188            }
189        }
190    } else {
191        println!("  [{}] No append log found.", "INFO".cyan());
192    }
193
194    println!("\n{}", "Status: Diagnostics complete.".bold());
195    println!("{}", "=".repeat(60));
196
197    Ok(())
198}