use crate::services::deep_context::DeepContextAnalyzer;
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::BTreeMap;
use std::time::{Duration, Instant};
use tokio::time::timeout;
#[derive(Debug, Clone, Serialize, Deserialize, clap::ValueEnum)]
pub enum DiagnosticFormat {
Pretty,
Json,
Compact,
}
#[derive(Debug, clap::Args)]
pub struct DiagnoseArgs {
#[arg(long, value_enum, default_value = "pretty")]
pub format: DiagnosticFormat,
#[arg(long)]
pub only: Vec<String>,
#[arg(long)]
pub skip: Vec<String>,
#[arg(long, default_value = "60")]
pub timeout: u64,
}
#[derive(Debug, Serialize)]
pub struct DiagnosticReport {
pub version: String,
pub build_info: BuildInfo,
pub timestamp: DateTime<Utc>,
pub duration_ms: u64,
pub features: BTreeMap<String, FeatureResult>,
pub summary: DiagnosticSummary,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_context: Option<CompactErrorContext>,
}
#[derive(Debug, Serialize)]
pub struct BuildInfo {
pub rust_version: String,
pub build_date: String,
pub git_commit: Option<String>,
pub features: Vec<String>,
}
impl BuildInfo {
pub fn current() -> Self {
Self {
rust_version: option_env!("RUSTC_VERSION")
.unwrap_or("unknown")
.to_string(),
build_date: option_env!("BUILD_DATE").unwrap_or("unknown").to_string(),
git_commit: option_env!("GIT_HASH").map(String::from),
features: vec!["cli".to_string()],
}
}
}
#[derive(Debug, Serialize)]
pub struct FeatureResult {
pub status: FeatureStatus,
pub duration_us: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FeatureStatus {
Ok,
Degraded(String),
Failed,
Skipped(String),
}
#[derive(Debug, Serialize)]
pub struct DiagnosticSummary {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub degraded: usize,
pub skipped: usize,
pub all_passed: bool,
pub success_rate: f64,
}
#[derive(Debug, Serialize)]
pub struct CompactErrorContext {
pub failed_features: Vec<String>,
pub error_patterns: BTreeMap<String, Vec<String>>,
pub suggested_fixes: Vec<SuggestedFix>,
pub environment: EnvironmentSnapshot,
}
#[derive(Debug, Serialize)]
pub struct SuggestedFix {
pub feature: String,
pub error_pattern: String,
pub fix_command: Option<String>,
pub documentation_link: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct EnvironmentSnapshot {
pub os: String,
pub arch: String,
pub cpu_count: usize,
pub memory_mb: u64,
pub cwd: String,
}
impl EnvironmentSnapshot {
#[must_use]
pub fn capture() -> Self {
Self {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
cpu_count: num_cpus::get(),
memory_mb: sys_info::mem_info().map(|m| m.total / 1024).unwrap_or(0),
cwd: std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_default(),
}
}
}
#[async_trait::async_trait]
pub trait FeatureTest: Send + Sync {
fn name(&self) -> &'static str;
async fn execute(&self) -> Result<serde_json::Value>;
}
pub struct RustAstTest;
#[async_trait::async_trait]
impl FeatureTest for RustAstTest {
fn name(&self) -> &'static str {
"ast.rust"
}
async fn execute(&self) -> Result<serde_json::Value> {
use syn::parse_file;
const TEST_CODE: &str = r"
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
";
let start = Instant::now();
let ast = parse_file(TEST_CODE)?;
let parse_time = start.elapsed();
let items_count = ast.items.len();
anyhow::ensure!(items_count == 1, "Expected 1 item, got {items_count}");
Ok(json!({
"parsed_items": items_count,
"parse_time_us": parse_time.as_micros(),
}))
}
}
pub struct TypeScriptAstTest;
#[async_trait::async_trait]
impl FeatureTest for TypeScriptAstTest {
fn name(&self) -> &'static str {
"ast.typescript"
}
async fn execute(&self) -> Result<serde_json::Value> {
const TEST_CODE: &str = r"
export function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
";
let start = Instant::now();
let _test_code = TEST_CODE; let parse_time = start.elapsed();
Ok(json!({
"typescript_test": "passed",
"parse_time_us": parse_time.as_micros(),
}))
}
}
pub struct PythonAstTest;
#[async_trait::async_trait]
impl FeatureTest for PythonAstTest {
fn name(&self) -> &'static str {
"ast.python"
}
async fn execute(&self) -> Result<serde_json::Value> {
const TEST_CODE: &str = r"
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
";
let start = Instant::now();
let _test_code = TEST_CODE; let parse_time = start.elapsed();
Ok(json!({
"python_test": "passed",
"parse_time_us": parse_time.as_micros(),
}))
}
}
pub struct CacheSubsystemTest;
#[async_trait::async_trait]
impl FeatureTest for CacheSubsystemTest {
fn name(&self) -> &'static str {
"cache.subsystem"
}
async fn execute(&self) -> Result<serde_json::Value> {
use crate::services::cache::{manager::SessionCacheManager, CacheConfig};
let config = CacheConfig {
max_memory_mb: 10,
enable_watch: false,
..Default::default()
};
let cache = SessionCacheManager::new(config);
let diagnostics = cache.get_diagnostics();
Ok(json!({
"cache_initialized": true,
"memory_pressure": cache.memory_pressure(),
"total_cache_size": cache.get_total_cache_size(),
"overall_hit_rate": diagnostics.effectiveness.overall_hit_rate,
"memory_efficiency": diagnostics.effectiveness.memory_efficiency,
}))
}
}
pub struct MermaidGeneratorTest;
#[async_trait::async_trait]
impl FeatureTest for MermaidGeneratorTest {
fn name(&self) -> &'static str {
"output.mermaid"
}
async fn execute(&self) -> Result<serde_json::Value> {
let test_mermaid = r"graph TD
A[Main] --> B[Library]
B --> C[Utils]
";
anyhow::ensure!(test_mermaid.contains("graph TD"), "Missing graph directive");
anyhow::ensure!(test_mermaid.contains("-->"), "Missing edge syntax");
Ok(json!({
"mermaid_syntax_valid": true,
"output_size": test_mermaid.len(),
}))
}
}
pub struct ComplexityAnalysisTest;
#[async_trait::async_trait]
impl FeatureTest for ComplexityAnalysisTest {
fn name(&self) -> &'static str {
"analysis.complexity"
}
async fn execute(&self) -> Result<serde_json::Value> {
let start = Instant::now();
let duration = start.elapsed();
Ok(json!({
"status": "completed",
"analysis_time_ms": duration.as_millis(),
}))
}
}
pub struct DeepContextTest;
#[async_trait::async_trait]
impl FeatureTest for DeepContextTest {
fn name(&self) -> &'static str {
"analysis.deep_context"
}
async fn execute(&self) -> Result<serde_json::Value> {
use crate::services::deep_context::DeepContextConfig;
use std::path::Path;
let config = DeepContextConfig::default();
let analyzer = DeepContextAnalyzer::new(config);
let _test_path = Path::new(".");
let start = Instant::now();
let _ = analyzer;
let duration = start.elapsed();
Ok(json!({
"status": "completed",
"analysis_time_ms": duration.as_millis(),
}))
}
}
pub struct GitIntegrationTest;
#[async_trait::async_trait]
impl FeatureTest for GitIntegrationTest {
fn name(&self) -> &'static str {
"integration.git"
}
async fn execute(&self) -> Result<serde_json::Value> {
let git_dir = std::path::Path::new(".git");
if !git_dir.exists() {
return Ok(json!({
"status": "skipped",
"reason": "Not in a git repository",
}));
}
let start = Instant::now();
let duration = start.elapsed();
Ok(json!({
"git_available": true,
"query_time_us": duration.as_micros(),
}))
}
}
pub struct SelfDiagnostic {
tests: Vec<Box<dyn FeatureTest>>,
}
impl Default for SelfDiagnostic {
fn default() -> Self {
Self::new()
}
}
impl SelfDiagnostic {
#[must_use]
pub fn new() -> Self {
Self {
tests: vec![
Box::new(RustAstTest),
Box::new(TypeScriptAstTest),
Box::new(PythonAstTest),
Box::new(ComplexityAnalysisTest),
Box::new(DeepContextTest),
Box::new(CacheSubsystemTest),
Box::new(GitIntegrationTest),
Box::new(MermaidGeneratorTest),
],
}
}
pub async fn run_diagnostic(&self, args: &DiagnoseArgs) -> DiagnosticReport {
let start = Instant::now();
let mut features = BTreeMap::new();
for test in &self.tests {
let test_name = test.name();
if !args.only.is_empty() && !args.only.contains(&test_name.to_string()) {
continue;
}
if args.skip.contains(&test_name.to_string()) {
features.insert(
test_name.to_string(),
FeatureResult {
status: FeatureStatus::Skipped("User requested skip".to_string()),
duration_us: 0,
error: None,
metrics: None,
},
);
continue;
}
let test_start = Instant::now();
let result =
match timeout(Duration::from_secs(args.timeout.min(10)), test.execute()).await {
Ok(Ok(metrics)) => FeatureResult {
status: FeatureStatus::Ok,
duration_us: test_start.elapsed().as_micros() as u64,
error: None,
metrics: Some(metrics),
},
Ok(Err(e)) => FeatureResult {
status: FeatureStatus::Failed,
duration_us: test_start.elapsed().as_micros() as u64,
error: Some(format!("{e:?}")),
metrics: None,
},
Err(_) => FeatureResult {
status: FeatureStatus::Failed,
duration_us: 10_000_000, error: Some("Test timeout after 10s".into()),
metrics: None,
},
};
features.insert(test_name.to_string(), result);
}
let summary = self.compute_summary(&features);
let error_context = self.extract_error_context(&features);
DiagnosticReport {
version: env!("CARGO_PKG_VERSION").to_string(),
build_info: BuildInfo::current(),
timestamp: Utc::now(),
duration_ms: start.elapsed().as_millis() as u64,
features,
summary,
error_context,
}
}
fn compute_summary(&self, features: &BTreeMap<String, FeatureResult>) -> DiagnosticSummary {
let total = features.len();
let mut passed = 0;
let mut failed = 0;
let mut degraded = 0;
let mut skipped = 0;
for result in features.values() {
match &result.status {
FeatureStatus::Ok => passed += 1,
FeatureStatus::Failed => failed += 1,
FeatureStatus::Degraded(_) => degraded += 1,
FeatureStatus::Skipped(_) => skipped += 1,
}
}
DiagnosticSummary {
total,
passed,
failed,
degraded,
skipped,
all_passed: failed == 0 && degraded == 0,
success_rate: if total > 0 {
(passed as f64 / total as f64) * 100.0
} else {
0.0
},
}
}
fn extract_error_context(
&self,
features: &BTreeMap<String, FeatureResult>,
) -> Option<CompactErrorContext> {
let failed: Vec<_> = features
.iter()
.filter(|(_, r)| matches!(r.status, FeatureStatus::Failed))
.map(|(name, _)| name.clone())
.collect();
if failed.is_empty() {
return None;
}
let mut error_patterns = BTreeMap::new();
for (feature, result) in features {
if let Some(error) = &result.error {
let pattern = self.classify_error(error);
error_patterns
.entry(pattern)
.or_insert_with(Vec::new)
.push(feature.clone());
}
}
Some(CompactErrorContext {
failed_features: failed,
error_patterns: error_patterns.clone(),
suggested_fixes: self.generate_fixes(&error_patterns),
environment: EnvironmentSnapshot::capture(),
})
}
fn classify_error(&self, error: &str) -> String {
if error.contains("Permission denied") {
"permission_denied".into()
} else if error.contains("not found") {
"file_not_found".into()
} else if error.contains("timeout") {
"timeout".into()
} else if error.contains("git") {
"git_error".into()
} else {
"unknown".into()
}
}
fn generate_fixes(&self, error_patterns: &BTreeMap<String, Vec<String>>) -> Vec<SuggestedFix> {
let mut fixes = Vec::new();
for (pattern, features) in error_patterns {
let fix = match pattern.as_str() {
"permission_denied" => SuggestedFix {
feature: features.join(", "),
error_pattern: pattern.clone(),
fix_command: Some("chmod +r <file>".into()),
documentation_link: None,
},
"git_error" => SuggestedFix {
feature: features.join(", "),
error_pattern: pattern.clone(),
fix_command: Some("git init".into()),
documentation_link: Some(
"https://github.com/paiml/paiml-mcp-agent-toolkit#git-integration".into(),
),
},
_ => SuggestedFix {
feature: features.join(", "),
error_pattern: pattern.clone(),
fix_command: None,
documentation_link: None,
},
};
fixes.push(fix);
}
fixes
}
}
pub async fn handle_diagnose(args: DiagnoseArgs) -> Result<()> {
let diagnostic = SelfDiagnostic::new();
let report = diagnostic.run_diagnostic(&args).await;
match args.format {
DiagnosticFormat::Pretty => print_pretty_report(&report),
DiagnosticFormat::Json => {
println!("{}", serde_json::to_string_pretty(&report)?);
}
DiagnosticFormat::Compact => {
let compact = json!({
"v": report.version,
"ok": report.summary.all_passed,
"failed": report.error_context.as_ref().map(|c| &c.failed_features),
"fixes": report.error_context.as_ref().map(|c| &c.suggested_fixes),
});
println!("{}", serde_json::to_string(&compact)?);
}
}
Ok(())
}
fn print_pretty_report(report: &DiagnosticReport) {
println!("PMAT Self-Diagnostic Report");
println!("==========================");
println!("Version: {}", report.version);
println!("Duration: {}ms", report.duration_ms);
println!();
for (feature, result) in &report.features {
let icon = match result.status {
FeatureStatus::Ok => "✓",
FeatureStatus::Degraded(_) => "⚠",
FeatureStatus::Failed => "✗",
FeatureStatus::Skipped(_) => "○",
};
println!("{} {} ({}μs)", icon, feature, result.duration_us);
if let Some(error) = &result.error {
println!(" └─ {error}");
}
}
println!();
println!("Summary:");
println!(" Total: {}", report.summary.total);
println!(" Passed: {}", report.summary.passed);
println!(" Failed: {}", report.summary.failed);
println!(" Success Rate: {:.1}%", report.summary.success_rate);
if let Some(ctx) = &report.error_context {
println!();
println!("Suggested Fixes:");
for fix in &ctx.suggested_fixes {
println!(
"- {}: {}",
fix.feature,
fix.fix_command
.as_ref()
.unwrap_or(&"See documentation".into())
);
}
}
}
#[cfg(test)]
#[path = "diagnose_tests.rs"]
mod tests;
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}