the-code-graph-cli 0.1.2

Command-line interface for The Code Graph
Documentation
use domain::error::{CodeGraphError, Result};
use eval::report::SuiteResult;
use eval::{Suite, SuiteConfig};
use std::io::Write;

use super::EvalArgs;
use crate::output::{Displayable, OutputFormat};

fn suite_from_str(s: &str) -> Result<Suite> {
    match s {
        "search" => Ok(Suite::Search),
        "impact" => Ok(Suite::Impact),
        "all" => Ok(Suite::All),
        _ => Err(CodeGraphError::Other(format!(
            "Unknown suite '{}'. Valid: search, impact, all",
            s
        ))),
    }
}

impl Displayable for SuiteResult {
    fn fmt_compact(&self, w: &mut dyn Write) -> std::io::Result<()> {
        self.fmt_compact(w)
    }
    fn fmt_table(&self, w: &mut dyn Write) -> std::io::Result<()> {
        self.fmt_table(w)
    }
    fn fmt_json(&self, w: &mut dyn Write) -> std::io::Result<()> {
        self.fmt_json(w)
    }
}

pub fn run_eval(args: &EvalArgs, output_format: OutputFormat) -> Result<()> {
    let suite = suite_from_str(&args.suite)?;
    let suites_dir = find_suites_dir()?;

    let config = SuiteConfig {
        suite,
        no_cache: args.no_cache,
        suites_dir,
        search_limit: 20,
    };

    let result = eval::run_suite(&config)?;
    crate::output::print(&result, output_format);

    if !result.all_passed() {
        return Err(CodeGraphError::Other(
            "Quality targets not met — see results above".into(),
        ));
    }
    Ok(())
}

fn find_suites_dir() -> Result<std::path::PathBuf> {
    let cwd_suites = std::path::PathBuf::from("eval/suites");
    if cwd_suites.is_dir() {
        return Ok(cwd_suites);
    }
    if let Ok(exe) = std::env::current_exe() {
        let exe_suites = exe.parent().unwrap_or(exe.as_ref()).join("eval/suites");
        if exe_suites.is_dir() {
            return Ok(exe_suites);
        }
    }
    Err(CodeGraphError::Other(
        "eval/suites/ directory not found — run from project root".into(),
    ))
}

#[cfg(test)]
mod tests {
    use super::*;
    use eval::report::{ImpactSuiteResult, SearchSuiteResult, SuiteResult};

    #[test]
    fn suite_from_string_search() {
        let suite = suite_from_str("search");
        assert!(suite.is_ok());
        assert!(matches!(suite.unwrap(), Suite::Search));
    }

    #[test]
    fn suite_from_string_impact() {
        let suite = suite_from_str("impact");
        assert!(suite.is_ok());
        assert!(matches!(suite.unwrap(), Suite::Impact));
    }

    #[test]
    fn suite_from_string_all() {
        let suite = suite_from_str("all");
        assert!(suite.is_ok());
        assert!(matches!(suite.unwrap(), Suite::All));
    }

    #[test]
    fn suite_from_string_invalid() {
        let suite = suite_from_str("unknown");
        assert!(suite.is_err());
        let msg = format!("{}", suite.unwrap_err());
        assert!(msg.contains("Unknown suite 'unknown'"));
    }

    fn sample_suite_result() -> SuiteResult {
        SuiteResult {
            search: Some(SearchSuiteResult {
                repos: 3,
                queries: 30,
                mrr: 0.65,
                precision_at_5: 0.72,
                precision_at_10: 0.60,
                mrr_target: 0.30,
                mrr_passed: true,
                per_category: vec![],
            }),
            impact: Some(ImpactSuiteResult {
                repos: 3,
                scenarios: 15,
                precision: 0.55,
                recall: 0.45,
                f1: 0.50,
                precision_target: 0.40,
                precision_passed: true,
            }),
        }
    }

    #[test]
    fn displayable_compact_output() {
        let result = sample_suite_result();
        let mut buf = Vec::new();
        Displayable::fmt_compact(&result, &mut buf).unwrap();
        let output = String::from_utf8(buf).unwrap();
        assert!(output.contains("Search Suite"));
        assert!(output.contains("MRR"));
        assert!(output.contains("Impact Suite"));
        assert!(output.contains("Precision"));
    }

    #[test]
    fn displayable_json_output() {
        let result = sample_suite_result();
        let mut buf = Vec::new();
        Displayable::fmt_json(&result, &mut buf).unwrap();
        let output = String::from_utf8(buf).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(output.trim()).unwrap();
        assert!(parsed.get("search").is_some());
        assert!(parsed.get("impact").is_some());
    }
}