Skip to main content

allow_report/
migrate.rs

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}