req-cli 0.2.0

Managed requirements CLI for LLM agents and humans
// Implements REQ-0054 (project-level implementation-status summary with
// count and percentage per status bucket and a single delivery_progress %).
use anyhow::Result;
use serde_json::json;
use std::path::PathBuf;

use crate::cli::StatusArgs;
use crate::model::Status;
use crate::storage::load_resolved;

pub fn run(args: StatusArgs, file: &Option<PathBuf>) -> Result<()> {
    let (_, project) = load_resolved(file)?;
    let mut counts = [0usize; 6];
    // REQ-0092: --tag filter scopes the status report to a milestone slice.
    // Filter once so both counts and total reflect the scoped view.
    let scope: Vec<_> = project
        .requirements
        .values()
        .filter(|r| args.tag.iter().all(|t| r.tags.iter().any(|rt| rt == t)))
        .collect();
    let total = scope.len();
    for r in &scope {
        let i = match r.status {
            Status::Draft => 0,
            Status::Proposed => 1,
            Status::Approved => 2,
            Status::Implemented => 3,
            Status::Verified => 4,
            Status::Obsolete => 5,
        };
        counts[i] += 1;
    }
    let pct = |n: usize| {
        if total == 0 {
            0.0
        } else {
            100.0 * n as f64 / total as f64
        }
    };
    let non_obsolete = total - counts[5];
    let done = counts[3] + counts[4];
    let delivery_pct = if non_obsolete == 0 {
        0.0
    } else {
        100.0 * done as f64 / non_obsolete as f64
    };

    if args.json {
        println!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "project": project.name,
                "filter": { "tags": args.tag },
                "total": total,
                "by_status": {
                    "draft":       counts[0],
                    "proposed":    counts[1],
                    "approved":    counts[2],
                    "implemented": counts[3],
                    "verified":    counts[4],
                    "obsolete":    counts[5],
                },
                "delivery_progress_pct": (delivery_pct * 10.0).round() / 10.0,
                "non_obsolete": non_obsolete,
                "done": done,
            }))?
        );
        return Ok(());
    }

    println!("Project: {}", project.name);
    if !args.tag.is_empty() {
        println!("Scoped to tag(s): {}", args.tag.join(", "));
    }
    println!("Total:   {}", total);
    println!();
    println!(
        "  draft       : {:>4}  ({:>5.1}%)",
        counts[0],
        pct(counts[0])
    );
    println!(
        "  proposed    : {:>4}  ({:>5.1}%)",
        counts[1],
        pct(counts[1])
    );
    println!(
        "  approved    : {:>4}  ({:>5.1}%)",
        counts[2],
        pct(counts[2])
    );
    println!(
        "  implemented : {:>4}  ({:>5.1}%)",
        counts[3],
        pct(counts[3])
    );
    println!(
        "  verified    : {:>4}  ({:>5.1}%)",
        counts[4],
        pct(counts[4])
    );
    println!(
        "  obsolete    : {:>4}  ({:>5.1}%)",
        counts[5],
        pct(counts[5])
    );
    println!();
    println!(
        "Delivery progress: {:.1}%  ({} of {} non-obsolete are implemented or verified)",
        delivery_pct, done, non_obsolete
    );
    Ok(())
}