allow_report/
worklist_human.rs1use crate::evidence_reference_human::evidence_reference_human_status;
2use crate::worklist_summary::{
3 worklist_difficulty_count, worklist_kind_counts, worklist_risk_count,
4};
5use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, WorklistFilters, WorklistItem};
6
7pub fn render_worklist_human(
8 items: &[WorklistItem<'_>],
9 filters: WorklistFilters<'_>,
10 inventory: InventoryContext<'_>,
11) -> String {
12 let mut out = String::new();
13 out.push_str("cargo-allow worklist\n\n");
14 out.push_str(&format!(
15 "Inventory: source_tree/source_syntax via {}{}\n",
16 inventory.source,
17 worklist_inventory_files_suffix(inventory)
18 ));
19 if let Some(root) = inventory.root {
20 out.push_str(&format!("Source tree root: {root}\n"));
21 }
22 out.push_str(&worklist_filters_human(filters));
23 out.push_str(&format!("Work items: {}\n", items.len()));
24 out.push_str("Risk:\n");
25 out.push_str(&format!(
26 " high {}\n",
27 worklist_risk_count(items, "high")
28 ));
29 out.push_str(&format!(
30 " medium {}\n",
31 worklist_risk_count(items, "medium")
32 ));
33 out.push_str(&format!(
34 " low {}\n",
35 worklist_risk_count(items, "low")
36 ));
37 out.push_str("Difficulty:\n");
38 out.push_str(&format!(
39 " small {}\n",
40 worklist_difficulty_count(items, "small")
41 ));
42 out.push_str(&format!(
43 " medium {}\n",
44 worklist_difficulty_count(items, "medium")
45 ));
46 let kind_counts = worklist_kind_counts(items);
47 if !kind_counts.is_empty() {
48 out.push_str("Queue kinds:\n");
49 for (kind, count) in kind_counts {
50 out.push_str(&format!(" {kind:<26} {count}\n"));
51 }
52 }
53 for item in items.iter().take(80) {
54 out.push_str(&format!(
55 "\n{} ({}, {}) {}\n",
56 item.id, item.risk, item.difficulty, item.kind
57 ));
58 if let Some(path) = item.path {
59 out.push_str(&format!(" path: {path}\n"));
60 }
61 if let Some(package) = item.source_package {
62 out.push_str(&format!(" source package: {package}\n"));
63 }
64 if let Some(allow_id) = item.allow_id {
65 out.push_str(&format!(" allow: {allow_id}\n"));
66 }
67 if let Some(owner) = item.owner {
68 out.push_str(&format!(" owner: {owner}\n"));
69 }
70 if let Some(classification) = item.classification {
71 out.push_str(&format!(" classification: {classification}\n"));
72 }
73 if let Some(reason) = item.reason {
74 out.push_str(&format!(" reason: {reason}\n"));
75 }
76 if let Some(created) = item.created {
77 out.push_str(&format!(" created: {created}\n"));
78 }
79 if let Some(review_after) = item.review_after {
80 out.push_str(&format!(" review_after: {review_after}\n"));
81 }
82 if let Some(expires) = item.expires {
83 out.push_str(&format!(" expires: {expires}\n"));
84 }
85 if let Some(evidence_count) = item.evidence_count {
86 out.push_str(&format!(" evidence: {evidence_count} reference(s)\n"));
87 }
88 if let Some(selector_precision) = item.selector_precision {
89 out.push_str(&format!(" selector_precision: {selector_precision}\n"));
90 }
91 if let Some(reference) = item.evidence_reference.as_ref() {
92 let status = evidence_reference_human_status(reference);
93 out.push_str(&format!(
94 " evidence reference: {}: {} (status={}, prefix={}, target={})\n",
95 status.label,
96 reference.raw,
97 reference.status,
98 reference.prefix.unwrap_or("-"),
99 reference.target.unwrap_or("-")
100 ));
101 out.push_str(&format!(" evidence message: {}\n", reference.message));
102 }
103 if let Some(exception_kind) = item.exception_kind {
104 out.push_str(&format!(" exception: {exception_kind}"));
105 if let Some(family) = item.family {
106 out.push_str(&format!(".{family}"));
107 }
108 out.push('\n');
109 }
110 out.push_str(&format!(" status: {}\n", item.status));
111 out.push_str(&format!(" message: {}\n", item.message));
112 for action in item.suggested_actions.iter().take(2) {
113 out.push_str(&format!(" action: {action}\n"));
114 }
115 for command in item.proof_commands.iter().take(8) {
116 out.push_str(&format!(" proof: {command}\n"));
117 }
118 }
119 if items.len() > 80 {
120 out.push_str(&format!(
121 "\n{} additional work items omitted from human output; use `cargo-allow worklist --format json` for the full queue.\n",
122 items.len() - 80
123 ));
124 }
125 out.push('\n');
126 out.push_str(CLAIM_BOUNDARY_TEXT);
127 out.push('\n');
128 out
129}
130
131fn worklist_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
132 inventory
133 .files_scanned
134 .map(|files| format!("; files scanned: {files}"))
135 .unwrap_or_default()
136}
137
138fn worklist_filters_human(filters: WorklistFilters<'_>) -> String {
139 let mut parts = Vec::new();
140 if let Some(kind) = filters.kind {
141 parts.push(format!("kind={kind}"));
142 }
143 if let Some(family) = filters.family {
144 parts.push(format!("family={family}"));
145 }
146 if let Some(item_kind) = filters.item_kind {
147 parts.push(format!("item_kind={item_kind}"));
148 }
149 if let Some(status) = filters.status {
150 parts.push(format!("status={status}"));
151 }
152 if let Some(allow_id) = filters.allow_id {
153 parts.push(format!("allow_id={allow_id}"));
154 }
155 if let Some(path) = filters.path {
156 parts.push(format!("path={path}"));
157 }
158 if let Some(source_package) = filters.source_package {
159 parts.push(format!("source_package={source_package}"));
160 }
161 if let Some(owner) = filters.owner {
162 parts.push(format!("owner={owner}"));
163 }
164 if let Some(classification) = filters.classification {
165 parts.push(format!("classification={classification}"));
166 }
167 if filters.baseline_debt {
168 parts.push("baseline_debt=true".to_string());
169 }
170 if filters.broad_scope {
171 parts.push("broad_scope=true".to_string());
172 }
173 if let Some(risk) = filters.risk {
174 parts.push(format!("risk={risk}"));
175 }
176 if let Some(difficulty) = filters.difficulty {
177 parts.push(format!("difficulty={difficulty}"));
178 }
179 if filters.missing_evidence {
180 parts.push("missing_evidence=true".to_string());
181 }
182 if filters.broken_evidence {
183 parts.push("broken_evidence=true".to_string());
184 }
185 if filters.weak_evidence {
186 parts.push("weak_evidence=true".to_string());
187 }
188 if parts.is_empty() {
189 "Filters: none\n".to_string()
190 } else {
191 format!("Filters: {}\n", parts.join(", "))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::worklist_filters_human;
198 use crate::WorklistFilters;
199
200 #[test]
201 fn worklist_filters_human_records_every_filter_field() {
202 let text = worklist_filters_human(WorklistFilters {
203 kind: Some("unsafe"),
204 family: Some("unsafe_block"),
205 item_kind: Some("missing_evidence"),
206 status: Some("evidence_missing"),
207 allow_id: Some("allow-0001"),
208 path: Some("src/lib.rs"),
209 source_package: Some("allow-report"),
210 owner: Some("runtime"),
211 classification: Some("reviewed"),
212 baseline_debt: true,
213 broad_scope: true,
214 risk: Some("high"),
215 difficulty: Some("small"),
216 missing_evidence: true,
217 broken_evidence: true,
218 weak_evidence: true,
219 });
220
221 assert_eq!(
222 text,
223 "Filters: kind=unsafe, family=unsafe_block, item_kind=missing_evidence, status=evidence_missing, allow_id=allow-0001, path=src/lib.rs, source_package=allow-report, owner=runtime, classification=reviewed, baseline_debt=true, broad_scope=true, risk=high, difficulty=small, missing_evidence=true, broken_evidence=true, weak_evidence=true\n"
224 );
225 }
226
227 #[test]
228 fn worklist_filters_human_reports_none_when_no_filters_are_set() {
229 assert_eq!(
230 worklist_filters_human(WorklistFilters::default()),
231 "Filters: none\n"
232 );
233 }
234}