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}