agtrace-runtime 0.7.1

Internal runtime orchestration for the agtrace CLI. Not intended for direct use.
Documentation
use crate::{Error, Result};
use agtrace_engine::{DiagnoseResult, FailureExample, FailureType, categorize_parse_error};
use agtrace_providers::ProviderAdapter;
use agtrace_types::AgentEvent;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

#[derive(Debug, Clone)]
pub enum CheckStatus {
    Success,
    Failure,
}

#[derive(Debug, Clone)]
pub struct CheckResult {
    pub file_path: String,
    pub provider_name: String,
    pub status: CheckStatus,
    pub events: Vec<AgentEvent>,
    pub error_message: Option<String>,
}

#[derive(Debug, Clone)]
pub enum InspectContentType {
    Raw(String),
    Json(serde_json::Value),
}

#[derive(Debug, Clone)]
pub struct InspectLine {
    pub number: usize,
    pub content: InspectContentType,
}

#[derive(Debug, Clone)]
pub struct InspectResult {
    pub file_path: String,
    pub total_lines: usize,
    pub shown_lines: usize,
    pub lines: Vec<InspectLine>,
}

pub struct DoctorService;

impl DoctorService {
    pub fn diagnose_all(providers: &[(ProviderAdapter, PathBuf)]) -> Result<Vec<DiagnoseResult>> {
        let mut results = Vec::new();
        for (provider, root) in providers {
            if root.exists() {
                let res = Self::diagnose_provider(provider, root)?;
                results.push(res);
            }
        }
        Ok(results)
    }

    fn diagnose_provider(provider: &ProviderAdapter, log_root: &Path) -> Result<DiagnoseResult> {
        let mut all_files = Vec::new();

        for entry in WalkDir::new(log_root).into_iter().filter_map(|e| e.ok()) {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }

            if provider.discovery.probe(path).is_match() {
                all_files.push(path.to_path_buf());
            }
        }

        let files_to_check = all_files;

        let mut result = DiagnoseResult {
            provider_name: provider.id().to_string(),
            total_files: files_to_check.len(),
            successful: 0,
            failures: HashMap::new(),
        };

        for file_path in files_to_check {
            match Self::test_parse_file(provider, &file_path) {
                Ok(_) => {
                    result.successful += 1;
                }
                Err((failure_type, reason)) => {
                    result
                        .failures
                        .entry(failure_type)
                        .or_default()
                        .push(FailureExample {
                            path: file_path.display().to_string(),
                            reason,
                        });
                }
            }
        }

        Ok(result)
    }

    fn test_parse_file(
        provider: &ProviderAdapter,
        path: &Path,
    ) -> std::result::Result<(), (FailureType, String)> {
        match provider.parser.parse_file(path) {
            Ok(_events) => Ok(()),
            Err(e) => {
                let error_msg = format!("{:?}", e);
                Err(categorize_parse_error(&error_msg))
            }
        }
    }

    pub fn check_file(
        file_path: &str,
        provider: &ProviderAdapter,
        provider_name: &str,
    ) -> Result<CheckResult> {
        let path = Path::new(file_path);

        if !path.exists() {
            return Err(Error::InvalidOperation(format!(
                "File not found: {}",
                file_path
            )));
        }

        match provider.parser.parse_file(path) {
            Ok(events) => Ok(CheckResult {
                file_path: file_path.to_string(),
                provider_name: provider_name.to_string(),
                status: CheckStatus::Success,
                events,
                error_message: None,
            }),
            Err(e) => Ok(CheckResult {
                file_path: file_path.to_string(),
                provider_name: provider_name.to_string(),
                status: CheckStatus::Failure,
                events: vec![],
                error_message: Some(format!("{:#}", e)),
            }),
        }
    }

    pub fn inspect_file(file_path: &str, lines: usize, json_format: bool) -> Result<InspectResult> {
        let path = Path::new(file_path);

        if !path.exists() {
            return Err(Error::InvalidOperation(format!(
                "File not found: {}",
                file_path
            )));
        }

        let file = File::open(path)?;
        let reader = BufReader::new(file);

        let total_lines = std::fs::read_to_string(path)?.lines().count();

        let mut rendered_lines = Vec::new();
        for (idx, line) in reader.lines().take(lines).enumerate() {
            let line = line?;
            let content = if json_format {
                match serde_json::from_str::<serde_json::Value>(&line) {
                    Ok(json) => InspectContentType::Json(json),
                    Err(_) => InspectContentType::Raw(line.clone()),
                }
            } else {
                InspectContentType::Raw(line.clone())
            };
            rendered_lines.push(InspectLine {
                number: idx + 1,
                content,
            });
        }

        Ok(InspectResult {
            file_path: file_path.to_string(),
            total_lines,
            shown_lines: rendered_lines.len(),
            lines: rendered_lines,
        })
    }
}