govctl 0.7.5

Project governance CLI for RFC, ADR, and Work Item management
//! Verification guard command and work-item enforcement.

use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLevel};
use crate::parse::{load_guards_with_warnings, load_work_items};
use crate::ui;
use crate::verification;
use std::collections::HashMap;

pub fn verify(
    config: &Config,
    guard_ids: &[String],
    work_id: Option<&str>,
) -> anyhow::Result<Vec<Diagnostic>> {
    let (guards_by_id, mut diagnostics) = load_guard_context(config)?;
    let work_item = if let Some(work_id) = work_id {
        Some(load_work_item_by_id(config, work_id)?)
    } else {
        None
    };

    if let Some(work_item) = work_item.as_ref() {
        diagnostics.extend(verification::validate_work_item_verification(
            config,
            &guards_by_id,
            work_item,
        ));
    }

    if diagnostics
        .iter()
        .any(|diag| diag.level == DiagnosticLevel::Error)
    {
        return Ok(diagnostics);
    }

    let selected_guard_ids = if let Some(work_item) = work_item.as_ref() {
        verification::effective_required_guard_ids(config, work_item)
    } else if !guard_ids.is_empty() {
        guard_ids.to_vec()
    } else {
        verification::configured_default_guard_ids(config)
    };

    if selected_guard_ids.is_empty() {
        diagnostics.push(Diagnostic::new(
            DiagnosticCode::E0801MissingRequiredArg,
            "No verification guards selected",
            work_id.unwrap_or("gov/config.toml"),
        ));
        return Ok(diagnostics);
    }

    diagnostics.extend(run_selected_guards(
        config,
        &guards_by_id,
        &selected_guard_ids,
        work_id.unwrap_or("verify"),
    )?);
    Ok(diagnostics)
}

pub fn enforce_work_item_guards(
    config: &Config,
    work_item: &crate::model::WorkItemEntry,
) -> anyhow::Result<()> {
    let (guards_by_id, mut diagnostics) = load_guard_context(config)?;
    diagnostics.extend(verification::validate_work_item_verification(
        config,
        &guards_by_id,
        work_item,
    ));
    let mut errors: Vec<String> = diagnostics
        .into_iter()
        .filter(|diag| diag.level == DiagnosticLevel::Error)
        .map(|diag| diag.to_string())
        .collect();

    let selected_guard_ids = verification::effective_required_guard_ids(config, work_item);
    if selected_guard_ids.is_empty() {
        return Ok(());
    }

    let run_diags = run_selected_guards(
        config,
        &guards_by_id,
        &selected_guard_ids,
        &work_item.spec.govctl.id,
    )?;
    errors.extend(
        run_diags
            .into_iter()
            .filter(|diag| diag.level == DiagnosticLevel::Error)
            .map(|diag| diag.message),
    );

    if errors.is_empty() {
        Ok(())
    } else {
        Err(Diagnostic::new(
            DiagnosticCode::E1004GuardCheckFailed,
            format!(
                "Cannot mark as done: verification guard requirements failed:\n{}",
                errors
                    .into_iter()
                    .map(|msg| format!("  - {msg}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            ),
            &work_item.spec.govctl.id,
        )
        .into())
    }
}

fn load_guard_context(
    config: &Config,
) -> anyhow::Result<(HashMap<String, crate::model::GuardEntry>, Vec<Diagnostic>)> {
    let guard_result = load_guards_with_warnings(config)?;
    let mut diagnostics = guard_result.warnings;
    let (guards_by_id, guard_diags) = verification::build_guard_index(guard_result.items);
    diagnostics.extend(guard_diags);
    diagnostics.extend(verification::validate_verification_config(
        config,
        &guards_by_id,
    ));

    Ok((guards_by_id, diagnostics))
}

fn run_selected_guards(
    config: &Config,
    guards_by_id: &std::collections::HashMap<String, crate::model::GuardEntry>,
    selected_guard_ids: &[String],
    location: &str,
) -> anyhow::Result<Vec<Diagnostic>> {
    let mut diagnostics = Vec::new();

    for guard_id in selected_guard_ids {
        let Some(guard) = guards_by_id.get(guard_id) else {
            diagnostics.push(verification::unknown_guard_diagnostic(guard_id, location));
            continue;
        };

        let result = verification::run_guard(config, guard)?;
        if result.passed {
            ui::info(format!("PASS {}", result.id));
            continue;
        }

        let message = if result.timed_out {
            format!(
                "Verification guard '{}' timed out after {} seconds",
                result.id, guard.spec.check.timeout_secs
            )
        } else {
            format!("Verification guard '{}' failed", result.id)
        };
        let code = if result.timed_out {
            DiagnosticCode::E1005GuardTimeout
        } else {
            DiagnosticCode::E1004GuardCheckFailed
        };
        let details = result.output.trim();
        diagnostics.push(Diagnostic::new(
            code,
            if details.is_empty() {
                message
            } else {
                format!("{message}: {details}")
            },
            guard.path.display().to_string(),
        ));
        ui::info(format!("FAIL {}", result.id));
    }

    Ok(diagnostics)
}

fn load_work_item_by_id(
    config: &Config,
    work_id: &str,
) -> anyhow::Result<crate::model::WorkItemEntry> {
    load_work_items(config)?
        .into_iter()
        .find(|item| item.spec.govctl.id == work_id)
        .ok_or_else(|| {
            Diagnostic::new(
                DiagnosticCode::E0402WorkNotFound,
                format!("Work item not found: {work_id}"),
                work_id,
            )
            .into()
        })
}