allow-report 0.1.6

Report and receipt rendering for cargo-allow source exception scans.
Documentation
use crate::contracts::PROPOSE_ARTIFACT;
use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
use crate::{CLAIM_BOUNDARY_TEXT, ProposeReport};
use allow_core::json_escape;

pub fn render_propose_human(report: ProposeReport<'_>) -> String {
    let mut out = String::new();
    out.push_str("cargo-allow propose summary\n");
    out.push_str(&format!(
        "inventory: {}/{} via {}{}\n",
        report.inventory.scope,
        report.inventory.scanner,
        report.inventory.source,
        propose_inventory_files_suffix(report.inventory)
    ));
    if let Some(root) = report.inventory.root {
        out.push_str(&format!("source_tree_root: {root}\n"));
    }
    if let Some(kind) = report.kind {
        out.push_str(&format!("kind filter: {kind}\n"));
    }
    out.push_str(&format!("findings scanned: {}\n", report.findings_scanned));
    out.push_str(&format!(
        "baseline_debt entries proposed: {}\n",
        report.baseline_debt_entries_proposed
    ));
    out.push_str(&format!(
        "unsafe baseline_debt entries proposed: {}\n",
        report.unsafe_baseline_debt_entries_proposed
    ));
    out.push_str("owner: unowned\n");
    out.push_str("classification: baseline_debt\n");
    out.push_str("reason: Generated by cargo-allow propose; requires human review.\n");
    out.push_str(&format!("expires: {}\n", report.expires));
    if let Some(output) = report.policy_output {
        out.push_str(&format!("output: {output}\n"));
    } else {
        out.push_str("output: stdout\n");
    }
    append_propose_follow_up_queues_human(report, &mut out);
    out.push_str(
        "claim boundary: proposal only; generated debt still requires human review and evidence.\n",
    );
    out.push_str(CLAIM_BOUNDARY_TEXT);
    out.push('\n');
    out
}

fn propose_inventory_files_suffix(inventory: crate::InventoryContext<'_>) -> String {
    inventory
        .files_scanned
        .map(|files| format!("; files scanned: {files}"))
        .unwrap_or_default()
}

pub fn render_propose_json(report: ProposeReport<'_>) -> String {
    let mut out = String::new();
    out.push_str("{\n");
    push_json_fixed_artifact_preamble(&mut out, PROPOSE_ARTIFACT, report.inventory);
    out.push_str("  \"options\": {\n");
    out.push_str(&format!("    \"kind\": {},\n", option_json(report.kind)));
    out.push_str(&format!(
        "    \"expires\": \"{}\",\n",
        json_escape(report.expires)
    ));
    out.push_str(&format!(
        "    \"policy_output\": {},\n",
        option_json(report.policy_output)
    ));
    out.push_str(&format!("    \"force\": {}\n", bool_json(report.force)));
    out.push_str("  },\n");
    out.push_str("  \"summary\": {\n");
    out.push_str(&format!(
        "    \"findings_scanned\": {},\n",
        report.findings_scanned
    ));
    out.push_str(&format!(
        "    \"baseline_debt_entries_proposed\": {},\n",
        report.baseline_debt_entries_proposed
    ));
    out.push_str(&format!(
        "    \"unsafe_baseline_debt_entries_proposed\": {}\n",
        report.unsafe_baseline_debt_entries_proposed
    ));
    out.push_str("  },\n");
    append_propose_follow_up_queues_json(report, &mut out);
    out.push_str("  \"generated_entry_defaults\": {\n");
    out.push_str("    \"owner\": \"unowned\",\n");
    out.push_str("    \"classification\": \"baseline_debt\",\n");
    out.push_str("    \"reason\": \"Generated by cargo-allow propose; requires human review.\",\n");
    out.push_str(&format!(
        "    \"expires\": \"{}\"\n",
        json_escape(report.expires)
    ));
    out.push_str("  }\n");
    out.push_str("}\n");
    out
}

fn append_propose_follow_up_queues_human(report: ProposeReport<'_>, out: &mut String) {
    let queues = propose_follow_up_queues(report);
    if queues.is_empty() {
        return;
    }
    out.push_str("follow_up_queues:\n");
    for queue in queues {
        out.push_str(&format!("  {}\n", queue.command));
    }
}

fn append_propose_follow_up_queues_json(report: ProposeReport<'_>, out: &mut String) {
    let queues = propose_follow_up_queues(report);
    if queues.is_empty() {
        return;
    }
    out.push_str("  \"follow_up_queues\": [\n");
    for (index, queue) in queues.iter().enumerate() {
        if index > 0 {
            out.push_str(",\n");
        }
        out.push_str("    {\n");
        out.push_str(&format!(
            "      \"signal\": \"{}\",\n",
            json_escape(queue.signal)
        ));
        out.push_str(&format!(
            "      \"label\": \"{}\",\n",
            json_escape(queue.label)
        ));
        out.push_str(&format!(
            "      \"route_kind\": \"{}\",\n",
            json_escape(queue.route_kind)
        ));
        out.push_str(&format!(
            "      \"item_kind\": \"{}\",\n",
            json_escape(queue.item_kind)
        ));
        if let Some(worklist_filter) = queue.worklist_filter {
            out.push_str(&format!(
                "      \"worklist_filter\": \"{}\",\n",
                json_escape(worklist_filter)
            ));
        }
        out.push_str(&format!("      \"count\": {},\n", queue.count));
        out.push_str(&format!(
            "      \"command\": \"{}\"\n",
            json_escape(queue.command)
        ));
        out.push_str("    }");
    }
    out.push_str("\n  ],\n");
}

fn propose_follow_up_queues(report: ProposeReport<'_>) -> Vec<ProposeFollowUpQueue> {
    let mut queues = Vec::new();
    push_propose_follow_up_queue_if(
        &mut queues,
        ProposeFollowUpQueue {
            signal: "baseline_debt_entries_proposed",
            label: "baseline debt entries",
            route_kind: "worklist_filter",
            item_kind: "baseline_debt",
            worklist_filter: Some("baseline_debt"),
            count: report.baseline_debt_entries_proposed,
            command: "cargo-allow worklist --baseline-debt --format json",
        },
    );
    push_propose_follow_up_queue_if(
        &mut queues,
        ProposeFollowUpQueue {
            signal: "unsafe_baseline_debt_entries_proposed",
            label: "unsafe baseline debt entries",
            route_kind: "worklist_item_kind",
            item_kind: "weak_evidence_reference",
            worklist_filter: None,
            count: report.unsafe_baseline_debt_entries_proposed,
            command: "cargo-allow worklist --item-kind weak_evidence_reference --kind unsafe --format json",
        },
    );
    queues
}

fn push_propose_follow_up_queue_if(
    queues: &mut Vec<ProposeFollowUpQueue>,
    queue: ProposeFollowUpQueue,
) {
    if queue.count > 0 {
        queues.push(queue);
    }
}

struct ProposeFollowUpQueue {
    signal: &'static str,
    label: &'static str,
    route_kind: &'static str,
    item_kind: &'static str,
    worklist_filter: Option<&'static str>,
    count: usize,
    command: &'static str,
}