1use crate::diff_policy_detail::policy_change_detail;
2use crate::diff_posture::{diff_net_posture, diff_posture_summary};
3use crate::evidence_repair::evidence_repair_queues_from_counts;
4use crate::{DiffFindingChange, DiffPolicyChange};
5
6const DIFF_HUMAN_CHANGE_LIMIT: usize = 120;
7
8pub fn render_diff_posture_summary_human(
9 current_failures: usize,
10 finding_changes: &[DiffFindingChange<'_>],
11 policy_changes: &[DiffPolicyChange<'_>],
12) -> String {
13 render_diff_posture_summary_human_with_evidence_health(
14 current_failures,
15 0,
16 0,
17 finding_changes,
18 policy_changes,
19 )
20}
21
22pub fn render_diff_posture_summary_human_with_evidence_health_counts(
23 current_failures: usize,
24 broken_evidence_links: usize,
25 missing_evidence: usize,
26 weak_evidence_references: usize,
27 finding_changes: &[DiffFindingChange<'_>],
28 policy_changes: &[DiffPolicyChange<'_>],
29) -> String {
30 render_diff_posture_summary_human_with_evidence_health_counts_inner(
31 current_failures,
32 broken_evidence_links,
33 missing_evidence,
34 weak_evidence_references,
35 finding_changes,
36 policy_changes,
37 )
38}
39
40pub fn render_diff_posture_summary_human_with_evidence_health(
41 current_failures: usize,
42 broken_evidence_links: usize,
43 weak_evidence_references: usize,
44 finding_changes: &[DiffFindingChange<'_>],
45 policy_changes: &[DiffPolicyChange<'_>],
46) -> String {
47 render_diff_posture_summary_human_with_evidence_health_counts_inner(
48 current_failures,
49 broken_evidence_links,
50 0,
51 weak_evidence_references,
52 finding_changes,
53 policy_changes,
54 )
55}
56
57fn render_diff_posture_summary_human_with_evidence_health_counts_inner(
58 current_failures: usize,
59 broken_evidence_links: usize,
60 missing_evidence: usize,
61 weak_evidence_references: usize,
62 finding_changes: &[DiffFindingChange<'_>],
63 policy_changes: &[DiffPolicyChange<'_>],
64) -> String {
65 let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
66 let posture = diff_net_posture(summary);
67 let mut out = String::new();
68 out.push_str("\nDiff posture summary:\n");
69 out.push_str(&format!(" net_posture: {}\n", posture.as_str()));
70 out.push_str(&format!(
71 " reviewer_action: {}\n",
72 posture.reviewer_action()
73 ));
74 out.push_str(&format!(
75 " current_check_failures: {}\n",
76 summary.current_failures
77 ));
78 if broken_evidence_links > 0 {
79 out.push_str(&format!(
80 " broken_evidence_links: {broken_evidence_links}\n"
81 ));
82 }
83 if missing_evidence > 0 {
84 out.push_str(&format!(" missing_evidence: {missing_evidence}\n"));
85 }
86 if weak_evidence_references > 0 {
87 out.push_str(&format!(
88 " weak_evidence_references: {weak_evidence_references}\n"
89 ));
90 }
91 let evidence_repair_queues = evidence_repair_queues_from_counts(
92 broken_evidence_links,
93 missing_evidence,
94 weak_evidence_references,
95 );
96 if !evidence_repair_queues.is_empty() {
97 out.push_str(" evidence_repair_queues:\n");
98 for queue in evidence_repair_queues {
99 out.push_str(&format!(" {}\n", queue.command));
100 }
101 }
102 out.push_str(&format!(
103 " new_source_findings: {}\n",
104 summary.new_findings
105 ));
106 out.push_str(&format!(
107 " removed_source_findings: {}\n",
108 summary.removed_findings
109 ));
110 out.push_str(&format!(" policy_failures: {}\n", summary.policy_failures));
111 out.push_str(&format!(
112 " policy_review_items: {}\n",
113 summary.policy_review_items
114 ));
115 out.push_str(&format!(
116 " policy_improvements: {}\n",
117 summary.policy_improvements
118 ));
119 out
120}
121
122pub fn render_diff_finding_changes_human(changes: &[DiffFindingChange<'_>]) -> String {
123 let mut out = String::new();
124 out.push_str("\nFinding posture changes:\n");
125 if changes.is_empty() {
126 out.push_str(" none\n");
127 return out;
128 }
129 append_finding_changes_human_section(&mut out, "Finding attention", changes, "new");
130 append_finding_changes_human_section(&mut out, "Finding improvements", changes, "removed");
131 let known_changes = ["new", "removed"];
132 if changes
133 .iter()
134 .any(|change| !known_changes.contains(&change.change))
135 {
136 out.push_str(" Other finding changes:\n");
137 let other_count = changes
138 .iter()
139 .filter(|change| !known_changes.contains(&change.change))
140 .count();
141 for change in changes
142 .iter()
143 .filter(|change| !known_changes.contains(&change.change))
144 .take(DIFF_HUMAN_CHANGE_LIMIT)
145 {
146 append_finding_change_human_row(&mut out, change);
147 }
148 if other_count > DIFF_HUMAN_CHANGE_LIMIT {
149 out.push_str(&format!(
150 " ... {} more omitted\n",
151 other_count - DIFF_HUMAN_CHANGE_LIMIT
152 ));
153 }
154 }
155 out
156}
157
158fn append_finding_changes_human_section(
159 out: &mut String,
160 heading: &str,
161 changes: &[DiffFindingChange<'_>],
162 change_kind: &str,
163) {
164 if !changes.iter().any(|change| change.change == change_kind) {
165 return;
166 }
167 out.push_str(&format!(" {heading}:\n"));
168 let matching_count = changes
169 .iter()
170 .filter(|change| change.change == change_kind)
171 .count();
172 for change in changes
173 .iter()
174 .filter(|change| change.change == change_kind)
175 .take(DIFF_HUMAN_CHANGE_LIMIT)
176 {
177 append_finding_change_human_row(out, change);
178 }
179 if matching_count > DIFF_HUMAN_CHANGE_LIMIT {
180 out.push_str(&format!(
181 " ... {} more omitted\n",
182 matching_count - DIFF_HUMAN_CHANGE_LIMIT
183 ));
184 }
185}
186
187fn append_finding_change_human_row(out: &mut String, change: &DiffFindingChange<'_>) {
188 out.push_str(&format!(
189 " {} {}{} at {}\n",
190 change.change,
191 change.kind,
192 change
193 .family
194 .map(|family| format!(".{family}"))
195 .unwrap_or_default(),
196 change.path
197 ));
198}
199
200pub fn render_diff_policy_changes_human(changes: &[DiffPolicyChange<'_>]) -> String {
201 let mut out = String::new();
202 out.push_str("\nPolicy posture changes:\n");
203 if changes.is_empty() {
204 out.push_str(" none\n");
205 return out;
206 }
207 append_policy_changes_human_section(&mut out, "Policy failures", changes, "fail");
208 append_policy_changes_human_section(&mut out, "Policy review required", changes, "review");
209 append_policy_changes_human_section(&mut out, "Policy improvements", changes, "improvement");
210 let known_severities = ["fail", "review", "improvement"];
211 if changes
212 .iter()
213 .any(|change| !known_severities.contains(&change.severity))
214 {
215 out.push_str(" Other policy changes:\n");
216 for change in changes
217 .iter()
218 .filter(|change| !known_severities.contains(&change.severity))
219 {
220 append_policy_change_human_row(&mut out, change);
221 }
222 }
223 out
224}
225
226fn append_policy_changes_human_section(
227 out: &mut String,
228 heading: &str,
229 changes: &[DiffPolicyChange<'_>],
230 severity: &str,
231) {
232 if !changes.iter().any(|change| change.severity == severity) {
233 return;
234 }
235 out.push_str(&format!(" {heading}:\n"));
236 for change in changes {
237 if change.severity == severity {
238 append_policy_change_human_row(out, change);
239 }
240 }
241}
242
243fn append_policy_change_human_row(out: &mut String, change: &DiffPolicyChange<'_>) {
244 out.push_str(&format!(
245 " {} {} {}: {}\n",
246 change.severity, change.allow_id, change.kind, change.message
247 ));
248 if let Some(detail) = policy_change_detail(change) {
249 out.push_str(&format!(" detail: {detail}\n"));
250 }
251}