1use crate::contracts::MIGRATE_ARTIFACT;
2use crate::evidence_repair::evidence_repair_queues_from_counts;
3use crate::json::{bool_json, push_json_fixed_artifact_preamble};
4use crate::{CLAIM_BOUNDARY_TEXT, MigrateReport};
5use allow_core::json_escape;
6
7const BROKEN_EVIDENCE_LINK_COMMAND: &str =
8 "cargo-allow worklist --item-kind broken_evidence_link --format json";
9const UNSAFE_BROKEN_EVIDENCE_LINK_COMMAND: &str =
10 "cargo-allow worklist --item-kind broken_evidence_link --kind unsafe --format json";
11const WEAK_EVIDENCE_REFERENCE_COMMAND: &str =
12 "cargo-allow worklist --item-kind weak_evidence_reference --format json";
13const UNSAFE_WEAK_EVIDENCE_REFERENCE_COMMAND: &str =
14 "cargo-allow worklist --item-kind weak_evidence_reference --kind unsafe --format json";
15const BASELINE_DEBT_COMMAND: &str = "cargo-allow worklist --item-kind baseline_debt --format json";
16
17pub fn render_migrate_human(report: MigrateReport<'_>) -> String {
18 let mut out = String::new();
19 out.push_str("cargo-allow migrate summary\n");
20 out.push_str(&format!("input_kind: {}\n", report.input_kind));
21 out.push_str(&format!("input: {}\n", report.input_path));
22 out.push_str(&format!("output: {}\n", report.output_path));
23 out.push_str(&format!("force: {}\n", report.force));
24 out.push_str(&format!("allow_entries: {}\n", report.allow_entries));
25 out.push_str(&format!("baseline_debt: {}\n", report.baseline_debt));
26 out.push_str(&format!("unsafe_entries: {}\n", report.unsafe_entries));
27 out.push_str(&format!(
28 "lint_exception_entries: {}\n",
29 report.lint_exception_entries
30 ));
31 out.push_str(&format!(
32 "entries_with_evidence: {}\n",
33 report.entries_with_evidence
34 ));
35 out.push_str(&format!("evidence_entries: {}\n", report.evidence_entries));
36 out.push_str(&format!(
37 "entries_with_links: {}\n",
38 report.entries_with_links
39 ));
40 out.push_str(&format!("link_entries: {}\n", report.link_entries));
41 if let Some(count) = report.broken_evidence_links.filter(|count| *count > 0) {
42 out.push_str(&format!("broken_evidence_links: {count}\n"));
43 }
44 if let Some(count) = report
45 .unsafe_broken_evidence_links
46 .filter(|count| *count > 0)
47 {
48 out.push_str(&format!("unsafe_broken_evidence_links: {count}\n"));
49 }
50 if let Some(count) = report.weak_evidence_references.filter(|count| *count > 0) {
51 out.push_str(&format!("weak_evidence_references: {count}\n"));
52 }
53 if let Some(count) = report
54 .unsafe_weak_evidence_references
55 .filter(|count| *count > 0)
56 {
57 out.push_str(&format!("unsafe_weak_evidence_references: {count}\n"));
58 }
59 append_migrate_follow_up_queues_human(report, &mut out);
60 append_migrate_evidence_repair_queues_human(report, &mut out);
61 out.push_str(&format!(
62 "inventory: {}/{} via {}{}\n",
63 report.inventory.scope,
64 report.inventory.scanner,
65 report.inventory.source,
66 migrate_inventory_files_suffix(report.inventory)
67 ));
68 if let Some(root) = report.inventory.root {
69 out.push_str(&format!("source_tree_root: {root}\n"));
70 }
71 out.push_str(report.notes);
72 if !report.notes.ends_with('\n') {
73 out.push('\n');
74 }
75 out.push_str(CLAIM_BOUNDARY_TEXT);
76 out.push('\n');
77 out
78}
79
80fn append_migrate_follow_up_queues_human(report: MigrateReport<'_>, out: &mut String) {
81 let queues = migrate_follow_up_queues(report);
82 if queues.is_empty() {
83 return;
84 }
85 out.push_str("follow_up_queues:\n");
86 for queue in queues {
87 out.push_str(&format!(" {}\n", queue.command));
88 }
89}
90
91fn append_migrate_evidence_repair_queues_human(report: MigrateReport<'_>, out: &mut String) {
92 let commands = migrate_evidence_repair_commands(report);
93 if commands.is_empty() {
94 return;
95 }
96 out.push_str("evidence_repair_queues:\n");
97 for command in commands {
98 out.push_str(&format!(" {command}\n"));
99 }
100}
101
102fn migrate_evidence_repair_commands(report: MigrateReport<'_>) -> Vec<&'static str> {
103 let mut commands = Vec::new();
104 if report.broken_evidence_links.unwrap_or(0) > 0
105 || report.unsafe_broken_evidence_links.unwrap_or(0) > 0
106 {
107 commands.push(BROKEN_EVIDENCE_LINK_COMMAND);
108 }
109 if report.unsafe_broken_evidence_links.unwrap_or(0) > 0 {
110 commands.push(UNSAFE_BROKEN_EVIDENCE_LINK_COMMAND);
111 }
112 if report.weak_evidence_references.unwrap_or(0) > 0
113 || report.unsafe_weak_evidence_references.unwrap_or(0) > 0
114 {
115 commands.push(WEAK_EVIDENCE_REFERENCE_COMMAND);
116 }
117 if report.unsafe_weak_evidence_references.unwrap_or(0) > 0 {
118 commands.push(UNSAFE_WEAK_EVIDENCE_REFERENCE_COMMAND);
119 }
120 commands
121}
122
123fn migrate_inventory_files_suffix(inventory: crate::InventoryContext<'_>) -> String {
124 inventory
125 .files_scanned
126 .map(|files| format!("; files scanned: {files}"))
127 .unwrap_or_default()
128}
129
130pub fn render_migrate_json(report: MigrateReport<'_>) -> String {
131 let mut out = String::new();
132 out.push_str("{\n");
133 push_json_fixed_artifact_preamble(&mut out, MIGRATE_ARTIFACT, report.inventory);
134 out.push_str(" \"input\": {\n");
135 out.push_str(&format!(
136 " \"kind\": \"{}\",\n",
137 json_escape(report.input_kind)
138 ));
139 out.push_str(&format!(
140 " \"path\": \"{}\"\n",
141 json_escape(report.input_path)
142 ));
143 out.push_str(" },\n");
144 out.push_str(" \"output\": {\n");
145 out.push_str(&format!(
146 " \"path\": \"{}\",\n",
147 json_escape(report.output_path)
148 ));
149 out.push_str(&format!(" \"force\": {}\n", bool_json(report.force)));
150 out.push_str(" },\n");
151 out.push_str(" \"summary\": {\n");
152 out.push_str(&format!(
153 " \"allow_entries\": {},\n",
154 report.allow_entries
155 ));
156 out.push_str(&format!(
157 " \"baseline_debt\": {},\n",
158 report.baseline_debt
159 ));
160 out.push_str(&format!(
161 " \"unsafe_entries\": {},\n",
162 report.unsafe_entries
163 ));
164 out.push_str(&format!(
165 " \"lint_exception_entries\": {},\n",
166 report.lint_exception_entries
167 ));
168 let mut summary_tail = vec![format!(
169 " \"entries_with_evidence\": {}",
170 report.entries_with_evidence
171 )];
172 summary_tail.push(format!(
173 " \"evidence_entries\": {}",
174 report.evidence_entries
175 ));
176 summary_tail.push(format!(
177 " \"entries_with_links\": {}",
178 report.entries_with_links
179 ));
180 summary_tail.push(format!(" \"link_entries\": {}", report.link_entries));
181 for (name, count) in [
182 ("broken_evidence_links", report.broken_evidence_links),
183 (
184 "unsafe_broken_evidence_links",
185 report.unsafe_broken_evidence_links,
186 ),
187 ("weak_evidence_references", report.weak_evidence_references),
188 (
189 "unsafe_weak_evidence_references",
190 report.unsafe_weak_evidence_references,
191 ),
192 ] {
193 if let Some(count) = count.filter(|count| *count > 0) {
194 summary_tail.push(format!(" \"{name}\": {count}"));
195 }
196 }
197 out.push_str(&summary_tail.join(",\n"));
198 out.push('\n');
199 out.push_str(" },\n");
200 append_migrate_follow_up_queues_json(report, &mut out);
201 append_migrate_evidence_repair_queues_json(report, &mut out);
202 out.push_str(&format!(" \"notes\": \"{}\"\n", json_escape(report.notes)));
203 out.push_str("}\n");
204 out
205}
206
207fn append_migrate_follow_up_queues_json(report: MigrateReport<'_>, out: &mut String) {
208 let queues = migrate_follow_up_queues(report);
209 if queues.is_empty() {
210 return;
211 }
212
213 out.push_str(" \"follow_up_queues\": [\n");
214 for (index, queue) in queues.iter().enumerate() {
215 if index > 0 {
216 out.push_str(",\n");
217 }
218 out.push_str(" {\n");
219 out.push_str(&format!(
220 " \"signal\": \"{}\",\n",
221 json_escape(queue.signal)
222 ));
223 out.push_str(&format!(
224 " \"label\": \"{}\",\n",
225 json_escape(queue.label)
226 ));
227 out.push_str(&format!(
228 " \"route_kind\": \"{}\",\n",
229 json_escape(queue.route_kind)
230 ));
231 out.push_str(&format!(
232 " \"item_kind\": \"{}\",\n",
233 json_escape(queue.item_kind)
234 ));
235 out.push_str(&format!(" \"count\": {},\n", queue.count));
236 out.push_str(&format!(
237 " \"command\": \"{}\"\n",
238 json_escape(queue.command)
239 ));
240 out.push_str(" }");
241 }
242 out.push_str("\n ],\n");
243}
244
245fn append_migrate_evidence_repair_queues_json(report: MigrateReport<'_>, out: &mut String) {
246 let queues = migrate_evidence_repair_queues(report);
247 if queues.is_empty() {
248 return;
249 }
250
251 out.push_str(" \"evidence_repair_queues\": [\n");
252 for (index, queue) in queues.iter().enumerate() {
253 if index > 0 {
254 out.push_str(",\n");
255 }
256 out.push_str(" {\n");
257 out.push_str(&format!(
258 " \"signal\": \"{}\",\n",
259 json_escape(queue.signal)
260 ));
261 out.push_str(&format!(
262 " \"label\": \"{}\",\n",
263 json_escape(queue.label)
264 ));
265 out.push_str(&format!(
266 " \"route_kind\": \"{}\",\n",
267 json_escape(queue.route_kind)
268 ));
269 out.push_str(&format!(
270 " \"item_kind\": \"{}\",\n",
271 json_escape(queue.item_kind)
272 ));
273 out.push_str(&format!(" \"count\": {},\n", queue.count));
274 out.push_str(&format!(
275 " \"unsafe_count\": {},\n",
276 queue.unsafe_count
277 ));
278 out.push_str(&format!(
279 " \"command\": \"{}\"",
280 json_escape(queue.command)
281 ));
282 if let Some(unsafe_command) = queue.unsafe_command {
283 out.push_str(",\n");
284 out.push_str(&format!(
285 " \"unsafe_command\": \"{}\"",
286 json_escape(unsafe_command)
287 ));
288 }
289 out.push('\n');
290 out.push_str(" }");
291 }
292 out.push_str("\n ],\n");
293}
294
295fn migrate_follow_up_queues(report: MigrateReport<'_>) -> Vec<MigrateFollowUpQueue> {
296 let mut queues = Vec::new();
297 if report.baseline_debt > 0 {
298 queues.push(MigrateFollowUpQueue {
299 signal: "baseline_debt",
300 label: "baseline debt entries",
301 route_kind: "worklist_item_kind",
302 item_kind: "baseline_debt",
303 count: report.baseline_debt,
304 command: BASELINE_DEBT_COMMAND,
305 });
306 }
307 queues
308}
309
310fn migrate_evidence_repair_queues(report: MigrateReport<'_>) -> Vec<MigrateEvidenceRepairQueue> {
311 let mut queues = Vec::new();
312 let broken_count = report.broken_evidence_links.unwrap_or(0);
313 let unsafe_broken_count = report.unsafe_broken_evidence_links.unwrap_or(0);
314 let weak_count = report.weak_evidence_references.unwrap_or(0);
315 let unsafe_weak_count = report.unsafe_weak_evidence_references.unwrap_or(0);
316 let broken_total = broken_count.max(unsafe_broken_count);
317 let weak_total = weak_count.max(unsafe_weak_count);
318 for queue in evidence_repair_queues_from_counts(broken_total, 0, weak_total) {
319 let Some(item_kind) = queue.item_kind else {
320 continue;
321 };
322 let (count, unsafe_count) = match item_kind {
323 "broken_evidence_link" => (broken_total, unsafe_broken_count),
324 "weak_evidence_reference" => (weak_total, unsafe_weak_count),
325 _ => (queue.count, 0),
326 };
327 let unsafe_command = match item_kind {
328 "broken_evidence_link" if unsafe_count > 0 => Some(UNSAFE_BROKEN_EVIDENCE_LINK_COMMAND),
329 "weak_evidence_reference" if unsafe_count > 0 => {
330 Some(UNSAFE_WEAK_EVIDENCE_REFERENCE_COMMAND)
331 }
332 _ => None,
333 };
334 queues.push(MigrateEvidenceRepairQueue {
335 signal: queue.signal,
336 label: queue.label,
337 route_kind: "worklist_item_kind",
338 item_kind,
339 count,
340 unsafe_count,
341 command: migrate_evidence_repair_command(item_kind),
342 unsafe_command,
343 });
344 }
345 queues
346}
347
348fn migrate_evidence_repair_command(item_kind: &str) -> &'static str {
349 match item_kind {
350 "broken_evidence_link" => BROKEN_EVIDENCE_LINK_COMMAND,
351 "weak_evidence_reference" => WEAK_EVIDENCE_REFERENCE_COMMAND,
352 _ => "cargo-allow worklist --format json",
353 }
354}
355
356struct MigrateFollowUpQueue {
357 signal: &'static str,
358 label: &'static str,
359 route_kind: &'static str,
360 item_kind: &'static str,
361 count: usize,
362 command: &'static str,
363}
364
365struct MigrateEvidenceRepairQueue {
366 signal: &'static str,
367 label: &'static str,
368 route_kind: &'static str,
369 item_kind: &'static str,
370 count: usize,
371 unsafe_count: usize,
372 command: &'static str,
373 unsafe_command: Option<&'static str>,
374}