Skip to main content

allow_report/
worklist_human.rs

1use 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}