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,
}