use serde::{Deserialize, Serialize}; use std::fs::{self, OpenOptions}; use std::io::Write; use std::path::Path; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use super::proxy::SystemStatsResponse; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ValidationStatus { Pass, Warn, Fail, Unverified, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MetricValidation { pub macmon_value: f64, pub reference_value: f64, pub diff_pct: f64, pub status: ValidationStatus, pub tolerance: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LayerValidation { pub name: String, pub requires_sudo: bool, pub metrics: std::collections::HashMap, pub summary: ValidationSummary, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationSummary { pub total: u32, pub passed: u32, pub failed: u32, pub unverified: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationResult { pub timestamp: String, pub layers: std::collections::HashMap>, pub overall_summary: ValidationSummary, } impl ValidationStatus { fn from_diff(diff_pct: f64, tolerance: f64) -> Self { if diff_pct <= tolerance { ValidationStatus::Pass } else if diff_pct <= tolerance * 2.0 { ValidationStatus::Warn } else { ValidationStatus::Fail } } } fn get_timestamp() -> String { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let dt = chrono::DateTime::from_timestamp(now as i64, 0).unwrap(); dt.format("%Y-%m-%dT%H:%M:%SZ").to_string() } fn write_log(message: &str) { let log_dir = std::env::var("MOMENTRY_DATA_DIR") .unwrap_or_else(|_| ".".to_string()); let log_path = Path::new(&log_dir).join("logs").join("validation.log"); if let Some(parent) = log_path.parent() { let _ = fs::create_dir_all(parent); } let timestamp = get_timestamp(); let log_line = format!("{} {}\n", timestamp, message); if let Ok(mut file) = OpenOptions::new() .create(true) .append(true) .open(&log_path) { let _ = file.write_all(log_line.as_bytes()); } } // ==================== Layer 1: Activity Monitor Validation ==================== // CPU formula decision (2026-08-31): // - macmon uses `cpu_scaled_ratio` (frequency-weighted: E-core work counts less due to lower freq) // - `top` uses simple user+sys ratio (no frequency weighting) // - These are intentionally different formulas; macmon's approach is more accurate for "actual work done" // - Therefore, CPU validation is marked as Unverified (different formula, not comparable) pub fn validate_cpu_usage_scaled_ratio() -> Option { write_log("[INFO] Layer 1: CPU uses cpu_scaled_ratio (frequency-weighted) - not comparable to top"); let macmon_value = 0.0; // Will be filled by caller write_log(&format!( "[INFO] Layer 1 CPU: macmon cpu_scaled_ratio={:.1}% (frequency-weighted formula)", macmon_value )); Some(MetricValidation { macmon_value, reference_value: 0.0, diff_pct: 0.0, status: ValidationStatus::Unverified, tolerance: 0.0, }) } // Memory formula decision (2026-08-31): // - macmon uses its own memory calculation (ram_usage from crate) // - Activity Monitor uses: wired + active + inactive + speculative - purgeable // - These formulas are intentionally different; macmon's approach is chosen as the baseline // - Therefore, Memory Used validation is marked as Unverified (different formula, not comparable) pub fn validate_memory_used_macmon_formula() -> Option { write_log("[INFO] Layer 1: Memory uses macmon formula - not comparable to Activity Monitor"); let macmon_value = 0.0; // Will be filled by caller write_log(&format!( "[INFO] Layer 1 Memory Used: macmon={:.1}% (macmon formula)", macmon_value )); Some(MetricValidation { macmon_value, reference_value: 0.0, diff_pct: 0.0, status: ValidationStatus::Unverified, tolerance: 0.0, }) } pub fn validate_swap_via_sysctl() -> Option { write_log("[INFO] Layer 1: Validating Swap via sysctl"); let output = Command::new("sysctl") .args(["-n", "vm.swapusage"]) .output() .ok()?; let stdout = String::from_utf8_lossy(&output.stdout); // Parse: total = 9216.00M used = 8420.00M free = 796.00M (encrypted) let parts: Vec<&str> = stdout.split_whitespace().collect(); let total_mb: f64 = parts.get(2) .and_then(|v| v.trim_end_matches('M').parse().ok()) .unwrap_or(0.0); let used_mb: f64 = parts.get(5) .and_then(|v| v.trim_end_matches('M').parse().ok()) .unwrap_or(0.0); let used_pct = if total_mb > 0.0 { (used_mb / total_mb) * 100.0 } else { 0.0 }; let macmon_value = 0.0; // Will be filled by caller let tolerance = 1.0; let diff_pct = (macmon_value - used_pct).abs(); write_log(&format!( "[INFO] Layer 1 Swap: macmon={:.1}%, sysctl={:.1}%, diff={:.2}%", macmon_value, used_pct, diff_pct )); Some(MetricValidation { macmon_value, reference_value: used_pct, diff_pct, status: ValidationStatus::from_diff(diff_pct, tolerance), tolerance, }) } pub fn validate_memory_total_via_sysctl() -> Option { write_log("[INFO] Layer 1: Validating Memory Total via sysctl"); let output = Command::new("sysctl") .args(["-n", "hw.memsize"]) .output() .ok()?; let stdout = String::from_utf8_lossy(&output.stdout); let total_bytes: u64 = stdout.trim().parse().ok()?; let total_mb = total_bytes as f64 / (1024.0 * 1024.0); let macmon_value = 0.0; // Will be filled by caller let tolerance = 0.0; // Exact match expected let diff_pct = (macmon_value - total_mb).abs(); write_log(&format!( "[INFO] Layer 1 Memory Total: macmon={:.0}MB, sysctl={:.0}MB, diff={:.0}MB", macmon_value, total_mb, diff_pct )); Some(MetricValidation { macmon_value, reference_value: total_mb, diff_pct, status: ValidationStatus::from_diff(diff_pct, 0.1), // Very small tolerance tolerance: 0.1, }) } // ==================== Layer 2: System API Validation ==================== pub fn validate_cpu_temp_via_ioreg() -> Option { write_log("[INFO] Layer 2: Validating CPU Temp via ioreg"); let output = Command::new("ioreg") .args(["-r", "-n", "AppleSMC"]) .output() .ok()?; let stdout = String::from_utf8_lossy(&output.stdout); // Look for temperature values let temp = stdout .lines() .find(|line| line.contains("temperature") || line.contains("TC0P") || line.contains("TC0H")) .and_then(|line| { // Extract numeric value let num_str: String = line .chars() .filter(|c| c.is_numeric() || *c == '.' || *c == '-') .collect(); num_str.parse::().ok() }) .unwrap_or(0.0); let macmon_value = 0.0; // Will be filled by caller let tolerance = 1.0; let diff_pct = (macmon_value - temp).abs(); write_log(&format!( "[INFO] Layer 2 CPU Temp: macmon={:.1}°C, ioreg={:.1}°C, diff={:.1}°C", macmon_value, temp, diff_pct )); Some(MetricValidation { macmon_value, reference_value: temp, diff_pct, status: ValidationStatus::from_diff(diff_pct, tolerance), tolerance, }) } pub fn validate_thermal_state_via_pmset() -> Option { write_log("[INFO] Layer 2: Validating Thermal State via pmset"); let output = Command::new("pmset") .args(["-g", "therm"]) .output() .ok()?; let stdout = String::from_utf8_lossy(&output.stdout); // Parse thermal state let state = if stdout.contains("CPU Power Load") { "Nominal" } else if stdout.contains("Warning") { "Warning" } else { "Nominal" }; // For thermal state, we just log the result (it's a string comparison) write_log(&format!("[INFO] Layer 2 Thermal State: {}", state)); None // Return None for string-based metrics } // ==================== Main Validation Function ==================== pub fn run_validation(layers: &[u32], macmon_stats: &SystemStatsResponse) -> ValidationResult { write_log(&format!("[INFO] Validation started: layers={:?}", layers)); let mut result = ValidationResult { timestamp: get_timestamp(), layers: std::collections::HashMap::new(), overall_summary: ValidationSummary { total: 0, passed: 0, failed: 0, unverified: 0, }, }; // Layer 1: Activity Monitor if layers.contains(&1) { let mut layer1 = LayerValidation { name: "Activity Monitor".to_string(), requires_sudo: false, metrics: std::collections::HashMap::new(), summary: ValidationSummary { total: 0, passed: 0, failed: 0, unverified: 0, }, }; // CPU - Unverified (different formula) if let Some(mut val) = validate_cpu_usage_scaled_ratio() { val.macmon_value = macmon_stats.cpu_used_percent; layer1.metrics.insert("cpu_usage".to_string(), val); } // Memory Used - Unverified (different formula) if let Some(mut val) = validate_memory_used_macmon_formula() { val.macmon_value = macmon_stats.memory_used_percent; layer1.metrics.insert("memory_used_percent".to_string(), val); } // Memory Total if let Some(mut val) = validate_memory_total_via_sysctl() { val.macmon_value = macmon_stats.memory_total_mb as f64; val.diff_pct = (val.macmon_value - val.reference_value).abs(); val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance); layer1.metrics.insert("memory_total_mb".to_string(), val); } // Swap if macmon_stats.swap_total_mb > 0 { if let Some(mut val) = validate_swap_via_sysctl() { val.macmon_value = macmon_stats.swap_used_percent; val.diff_pct = (val.macmon_value - val.reference_value).abs(); val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance); layer1.metrics.insert("swap_used_percent".to_string(), val); } } // Calculate summary for (_, metric) in &layer1.metrics { layer1.summary.total += 1; result.overall_summary.total += 1; match metric.status { ValidationStatus::Pass => { layer1.summary.passed += 1; result.overall_summary.passed += 1; } ValidationStatus::Warn | ValidationStatus::Fail => { layer1.summary.failed += 1; result.overall_summary.failed += 1; } ValidationStatus::Unverified => { layer1.summary.unverified += 1; result.overall_summary.unverified += 1; } } } result.layers.insert("layer1".to_string(), Some(layer1)); } // Layer 2: System API if layers.contains(&2) { let mut layer2 = LayerValidation { name: "System API".to_string(), requires_sudo: false, metrics: std::collections::HashMap::new(), summary: ValidationSummary { total: 0, passed: 0, failed: 0, unverified: 0, }, }; // CPU Temp if let Some(temp) = macmon_stats.cpu_temp { if let Some(mut val) = validate_cpu_temp_via_ioreg() { val.macmon_value = temp; val.diff_pct = (val.macmon_value - val.reference_value).abs(); val.status = ValidationStatus::from_diff(val.diff_pct, val.tolerance); layer2.metrics.insert("cpu_temp".to_string(), val); } } // Calculate summary for (_, metric) in &layer2.metrics { layer2.summary.total += 1; result.overall_summary.total += 1; match metric.status { ValidationStatus::Pass => { layer2.summary.passed += 1; result.overall_summary.passed += 1; } ValidationStatus::Warn | ValidationStatus::Fail => { layer2.summary.failed += 1; result.overall_summary.failed += 1; } ValidationStatus::Unverified => { layer2.summary.unverified += 1; result.overall_summary.unverified += 1; } } } result.layers.insert("layer2".to_string(), Some(layer2)); } // Layer 3: Requires sudo (not implemented in this phase) if layers.contains(&3) { result.layers.insert("layer3".to_string(), None); write_log("[INFO] Layer 3 validation not implemented yet"); } // Layer 4: Unverified if layers.contains(&4) { let layer4 = LayerValidation { name: "Unverified".to_string(), requires_sudo: false, metrics: std::collections::HashMap::new(), summary: ValidationSummary { total: 3, passed: 0, failed: 0, unverified: 3, }, }; result.layers.insert("layer4".to_string(), Some(layer4)); result.overall_summary.total += 3; result.overall_summary.unverified += 3; } write_log(&format!( "[INFO] Validation completed: {}/{} passed, {} failed, {} unverified", result.overall_summary.passed, result.overall_summary.total, result.overall_summary.failed, result.overall_summary.unverified )); result }