cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use allow_core::{AllowEntry, Finding, FindingKind, MatchOutcome, MatchStatus};

use crate::worklist;

#[derive(Debug, Clone, Copy, Default)]
pub(super) struct ExplainReferenceAttention {
    pub(super) has_broken_evidence: bool,
    pub(super) has_weak_evidence: bool,
    pub(super) has_evidence_outside_default_inventory: bool,
    pub(super) has_broken_link: bool,
    pub(super) has_weak_link: bool,
    pub(super) has_link_outside_default_inventory: bool,
}

pub(super) fn explain_next_steps(
    entry: &AllowEntry,
    findings: &[Finding],
    outcomes: &[MatchOutcome],
    references: ExplainReferenceAttention,
) -> (Vec<String>, Vec<String>) {
    let attention = outcomes
        .iter()
        .filter(|outcome| outcome.status != MatchStatus::Matched)
        .collect::<Vec<_>>();
    if let Some(outcome) = attention.first() {
        let finding = outcome.finding_index.and_then(|index| findings.get(index));
        let kind = worklist::work_item_kind(outcome, finding, Some(entry));
        return (
            worklist::suggested_actions_for_context(&kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(&kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if references.has_evidence_outside_default_inventory {
        let finding = findings.first();
        let kind = "broken_evidence_link";
        return (
            vec![
                "commit the referenced evidence file if it should support repository policy"
                    .to_string(),
                "or rerun with --include-untracked when intentionally reviewing local receipt artifacts"
                    .to_string(),
            ],
            untracked_proof_commands(entry, finding, kind),
        );
    }
    if references.has_link_outside_default_inventory {
        let finding = findings.first();
        let kind = "broken_evidence_link";
        return (
            vec![
                "commit the referenced traceability file if it should support repository policy"
                    .to_string(),
                "or rerun with --include-untracked when intentionally reviewing local traceability files"
                    .to_string(),
            ],
            untracked_proof_commands(entry, finding, kind),
        );
    }
    if references.has_broken_evidence {
        let finding = findings.first();
        let kind = "broken_evidence_link";
        return (
            worklist::suggested_actions(kind)
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if references.has_broken_link {
        let finding = findings.first();
        let kind = "broken_evidence_link";
        return (
            worklist::suggested_link_actions_for_context(kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if references.has_weak_evidence {
        let finding = findings.first();
        let kind = "weak_evidence_reference";
        return (
            worklist::suggested_actions_for_context(kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if references.has_weak_link {
        let finding = findings.first();
        let kind = "weak_evidence_reference";
        return (
            worklist::suggested_link_actions_for_context(kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if entry.classification == "baseline_debt" {
        let finding = findings.first();
        let kind = "baseline_debt";
        return (
            worklist::suggested_actions_for_context(kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    if entry.evidence.is_empty() {
        let finding = findings.first();
        let kind = if entry.kind == FindingKind::Unsafe {
            "unsafe_missing_evidence"
        } else {
            "missing_evidence"
        };
        return (
            worklist::suggested_actions_for_context(kind, finding, Some(entry))
                .into_iter()
                .take(2)
                .collect(),
            worklist::proof_commands(kind, finding, Some(entry))
                .into_iter()
                .take(5)
                .collect(),
        );
    }
    (Vec::new(), Vec::new())
}

fn untracked_proof_commands(
    entry: &AllowEntry,
    finding: Option<&Finding>,
    kind: &str,
) -> Vec<String> {
    let mut commands = vec![
        format!("cargo-allow explain {}", entry.id),
        format!("cargo-allow explain {} --include-untracked", entry.id),
        format!("cargo-allow worklist --allow-id {} --format json", entry.id),
        "cargo-allow check --include-untracked --mode no-new".to_string(),
    ];
    for command in worklist::proof_commands(kind, finding, Some(entry)) {
        if !commands.iter().any(|existing| existing == &command) {
            commands.push(command);
        }
    }
    commands
}