Skip to main content

allow_report/
propose.rs

1use crate::contracts::PROPOSE_ARTIFACT;
2use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
3use crate::{CLAIM_BOUNDARY_TEXT, ProposeReport};
4use allow_core::json_escape;
5
6pub fn render_propose_human(report: ProposeReport<'_>) -> String {
7    let mut out = String::new();
8    out.push_str("cargo-allow propose summary\n");
9    out.push_str(&format!(
10        "inventory: {}/{} via {}{}\n",
11        report.inventory.scope,
12        report.inventory.scanner,
13        report.inventory.source,
14        propose_inventory_files_suffix(report.inventory)
15    ));
16    if let Some(root) = report.inventory.root {
17        out.push_str(&format!("source_tree_root: {root}\n"));
18    }
19    if let Some(kind) = report.kind {
20        out.push_str(&format!("kind filter: {kind}\n"));
21    }
22    out.push_str(&format!("findings scanned: {}\n", report.findings_scanned));
23    out.push_str(&format!(
24        "baseline_debt entries proposed: {}\n",
25        report.baseline_debt_entries_proposed
26    ));
27    out.push_str(&format!(
28        "unsafe baseline_debt entries proposed: {}\n",
29        report.unsafe_baseline_debt_entries_proposed
30    ));
31    out.push_str("owner: unowned\n");
32    out.push_str("classification: baseline_debt\n");
33    out.push_str("reason: Generated by cargo-allow propose; requires human review.\n");
34    out.push_str(&format!("expires: {}\n", report.expires));
35    if let Some(output) = report.policy_output {
36        out.push_str(&format!("output: {output}\n"));
37    } else {
38        out.push_str("output: stdout\n");
39    }
40    append_propose_follow_up_queues_human(report, &mut out);
41    out.push_str(
42        "claim boundary: proposal only; generated debt still requires human review and evidence.\n",
43    );
44    out.push_str(CLAIM_BOUNDARY_TEXT);
45    out.push('\n');
46    out
47}
48
49fn propose_inventory_files_suffix(inventory: crate::InventoryContext<'_>) -> String {
50    inventory
51        .files_scanned
52        .map(|files| format!("; files scanned: {files}"))
53        .unwrap_or_default()
54}
55
56pub fn render_propose_json(report: ProposeReport<'_>) -> String {
57    let mut out = String::new();
58    out.push_str("{\n");
59    push_json_fixed_artifact_preamble(&mut out, PROPOSE_ARTIFACT, report.inventory);
60    out.push_str("  \"options\": {\n");
61    out.push_str(&format!("    \"kind\": {},\n", option_json(report.kind)));
62    out.push_str(&format!(
63        "    \"expires\": \"{}\",\n",
64        json_escape(report.expires)
65    ));
66    out.push_str(&format!(
67        "    \"policy_output\": {},\n",
68        option_json(report.policy_output)
69    ));
70    out.push_str(&format!("    \"force\": {}\n", bool_json(report.force)));
71    out.push_str("  },\n");
72    out.push_str("  \"summary\": {\n");
73    out.push_str(&format!(
74        "    \"findings_scanned\": {},\n",
75        report.findings_scanned
76    ));
77    out.push_str(&format!(
78        "    \"baseline_debt_entries_proposed\": {},\n",
79        report.baseline_debt_entries_proposed
80    ));
81    out.push_str(&format!(
82        "    \"unsafe_baseline_debt_entries_proposed\": {}\n",
83        report.unsafe_baseline_debt_entries_proposed
84    ));
85    out.push_str("  },\n");
86    append_propose_follow_up_queues_json(report, &mut out);
87    out.push_str("  \"generated_entry_defaults\": {\n");
88    out.push_str("    \"owner\": \"unowned\",\n");
89    out.push_str("    \"classification\": \"baseline_debt\",\n");
90    out.push_str("    \"reason\": \"Generated by cargo-allow propose; requires human review.\",\n");
91    out.push_str(&format!(
92        "    \"expires\": \"{}\"\n",
93        json_escape(report.expires)
94    ));
95    out.push_str("  }\n");
96    out.push_str("}\n");
97    out
98}
99
100fn append_propose_follow_up_queues_human(report: ProposeReport<'_>, out: &mut String) {
101    let queues = propose_follow_up_queues(report);
102    if queues.is_empty() {
103        return;
104    }
105    out.push_str("follow_up_queues:\n");
106    for queue in queues {
107        out.push_str(&format!("  {}\n", queue.command));
108    }
109}
110
111fn append_propose_follow_up_queues_json(report: ProposeReport<'_>, out: &mut String) {
112    let queues = propose_follow_up_queues(report);
113    if queues.is_empty() {
114        return;
115    }
116    out.push_str("  \"follow_up_queues\": [\n");
117    for (index, queue) in queues.iter().enumerate() {
118        if index > 0 {
119            out.push_str(",\n");
120        }
121        out.push_str("    {\n");
122        out.push_str(&format!(
123            "      \"signal\": \"{}\",\n",
124            json_escape(queue.signal)
125        ));
126        out.push_str(&format!(
127            "      \"label\": \"{}\",\n",
128            json_escape(queue.label)
129        ));
130        out.push_str(&format!(
131            "      \"route_kind\": \"{}\",\n",
132            json_escape(queue.route_kind)
133        ));
134        out.push_str(&format!(
135            "      \"item_kind\": \"{}\",\n",
136            json_escape(queue.item_kind)
137        ));
138        if let Some(worklist_filter) = queue.worklist_filter {
139            out.push_str(&format!(
140                "      \"worklist_filter\": \"{}\",\n",
141                json_escape(worklist_filter)
142            ));
143        }
144        out.push_str(&format!("      \"count\": {},\n", queue.count));
145        out.push_str(&format!(
146            "      \"command\": \"{}\"\n",
147            json_escape(queue.command)
148        ));
149        out.push_str("    }");
150    }
151    out.push_str("\n  ],\n");
152}
153
154fn propose_follow_up_queues(report: ProposeReport<'_>) -> Vec<ProposeFollowUpQueue> {
155    let mut queues = Vec::new();
156    push_propose_follow_up_queue_if(
157        &mut queues,
158        ProposeFollowUpQueue {
159            signal: "baseline_debt_entries_proposed",
160            label: "baseline debt entries",
161            route_kind: "worklist_filter",
162            item_kind: "baseline_debt",
163            worklist_filter: Some("baseline_debt"),
164            count: report.baseline_debt_entries_proposed,
165            command: "cargo-allow worklist --baseline-debt --format json",
166        },
167    );
168    push_propose_follow_up_queue_if(
169        &mut queues,
170        ProposeFollowUpQueue {
171            signal: "unsafe_baseline_debt_entries_proposed",
172            label: "unsafe baseline debt entries",
173            route_kind: "worklist_item_kind",
174            item_kind: "weak_evidence_reference",
175            worklist_filter: None,
176            count: report.unsafe_baseline_debt_entries_proposed,
177            command: "cargo-allow worklist --item-kind weak_evidence_reference --kind unsafe --format json",
178        },
179    );
180    queues
181}
182
183fn push_propose_follow_up_queue_if(
184    queues: &mut Vec<ProposeFollowUpQueue>,
185    queue: ProposeFollowUpQueue,
186) {
187    if queue.count > 0 {
188        queues.push(queue);
189    }
190}
191
192struct ProposeFollowUpQueue {
193    signal: &'static str,
194    label: &'static str,
195    route_kind: &'static str,
196    item_kind: &'static str,
197    worklist_filter: Option<&'static str>,
198    count: usize,
199    command: &'static str,
200}