agent-memgc 1.0.2

High-performance linter, cache-layout optimizer, and generational mark-and-sweep garbage collector for AI agent instruction files (AGENTS.md, CLAUDE.md, .cursorrules) in Rust.
Documentation
use crate::domain::finding::Severity;
use crate::engine::{fix_instructions, lint_file};
use colored::*;
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_FULL;
use comfy_table::{Cell, Color, Table};
use std::fs;
use std::path::Path;

pub fn cmd_check(path: &Path, project_root: &Path, json_mode: bool) -> anyhow::Result<bool> {
    let report = lint_file(path, project_root)?;

    if json_mode {
        println!("{}", serde_json::to_string_pretty(&report)?);
        return Ok(!report.has_errors());
    }

    println!("\n{}", format!("=== agent-memgc Instruction Audit: {} ===", path.display()).cyan().bold());
    println!("Lines Analyzed: {} | Findings: {} (Errors: {}, Warnings: {}, Info: {})",
        report.total_lines,
        report.findings.len(),
        report.errors_count.to_string().red().bold(),
        report.warnings_count.to_string().yellow().bold(),
        report.info_count.to_string().blue().bold()
    );

    if !report.cache_clean {
        println!("{}", "⚠️  Prompt Prefix Cache: UNHEALTHY (Volatile content found at instruction prefix)".yellow().bold());
    } else {
        println!("{}", "✅ Prompt Prefix Cache: OPTIMIZED (Static rules pinned to instruction prefix)".green().bold());
    }

    if report.estimated_wasted_tokens > 0 {
        println!("Estimated Wasted Tokens: ~{} tokens/turn\n", report.estimated_wasted_tokens.to_string().yellow());
    } else {
        println!();
    }

    if report.findings.is_empty() {
        println!("{}", "✨ All instruction rules passed! Zero dead paths, contradictions, or cache bloat.\n".green().bold());
        return Ok(true);
    }

    let mut table = Table::new();
    table.load_preset(UTF8_FULL).apply_modifier(UTF8_ROUND_CORNERS);
    table.set_header(vec![
        Cell::new("Rule").fg(Color::Cyan),
        Cell::new("Line").fg(Color::Yellow),
        Cell::new("Severity").fg(Color::Magenta),
        Cell::new("Message").fg(Color::White),
    ]);

    for f in &report.findings {
        let (sev_str, sev_color) = match f.severity {
            Severity::Error => ("ERROR", Color::Red),
            Severity::Warning => ("WARN", Color::Yellow),
            Severity::Info => ("INFO", Color::Blue),
        };

        table.add_row(vec![
            Cell::new(f.rule.as_str()).fg(Color::Cyan),
            Cell::new(f.line.to_string()).fg(Color::Yellow),
            Cell::new(sev_str).fg(sev_color),
            Cell::new(&f.message),
        ]);
    }

    println!("{}\n", table);

    println!("{}", "💡 Run `agent-memgc fix <file>` to automatically resolve dead paths and cache ordering.".cyan());
    Ok(!report.has_errors())
}

pub fn cmd_fix(path: &Path, project_root: &Path, dry_run: bool) -> anyhow::Result<()> {
    let content = fs::read_to_string(path)?;
    let (fixed_content, saved_tokens) = fix_instructions(&content, project_root);

    if content == fixed_content {
        println!("{}", "✨ File is already clean and optimized; no changes required.".green().bold());
        return Ok(());
    }

    if dry_run {
        println!("\n{}", format!("=== Dry-Run Fix Diff: {} ===", path.display()).cyan().bold());
        println!("{}", fixed_content);
        println!("\n{}", format!("Estimated Token Reduction: ~{} tokens/request", saved_tokens).green().bold());
        return Ok(());
    }

    fs::write(path, fixed_content)?;
    println!("{}", format!("✅ Successfully optimized {} (Saved ~{} tokens per agent turn).", path.display(), saved_tokens).green().bold());
    Ok(())
}

pub fn cmd_stats(path: &Path, project_root: &Path) -> anyhow::Result<()> {
    let report = lint_file(path, project_root)?;
    println!("\n{}", format!("=== Agent Instruction Quality Score: {} ===", path.display()).cyan().bold());

    let mut score: i32 = 100;
    score -= (report.errors_count as i32) * 20;
    score -= (report.warnings_count as i32) * 10;
    score -= (report.info_count as i32) * 5;
    let score = score.clamp(0, 100);

    let grade = match score {
        90..=100 => "A+ (Production Ready)",
        75..=89 => "B (Acceptable, minor bloat)",
        50..=74 => "C (Prone to agent hallucination)",
        _ => "F (Critical instruction decay)",
    };

    println!("Quality Score:  {}/100", score.to_string().bold());
    println!("Health Grade:   {}", grade.bold());
    println!("Token Footprint: ~{} lines / {} findings\n", report.total_lines, report.findings.len());
    Ok(())
}