1use crate::diff_finding_detail::structural_identity_summary;
2use crate::diff_policy_detail::policy_change_detail;
3use crate::diff_posture::{
4 diff_evidence_delta_summary, diff_net_posture, diff_posture_summary,
5 diff_structural_delta_summary,
6};
7use crate::evidence_repair::evidence_repair_queues_from_counts;
8use crate::{DiffFindingChange, DiffPolicyChange};
9
10const DIFF_HUMAN_CHANGE_LIMIT: usize = 120;
11
12pub fn render_diff_posture_summary_human(
13 current_failures: usize,
14 finding_changes: &[DiffFindingChange<'_>],
15 policy_changes: &[DiffPolicyChange<'_>],
16) -> String {
17 render_diff_posture_summary_human_with_evidence_health(
18 current_failures,
19 0,
20 0,
21 finding_changes,
22 policy_changes,
23 )
24}
25
26pub fn render_diff_posture_summary_human_with_evidence_health_counts(
27 current_failures: usize,
28 broken_evidence_links: usize,
29 missing_evidence: usize,
30 weak_evidence_references: usize,
31 finding_changes: &[DiffFindingChange<'_>],
32 policy_changes: &[DiffPolicyChange<'_>],
33) -> String {
34 render_diff_posture_summary_human_with_evidence_health_counts_inner(
35 current_failures,
36 broken_evidence_links,
37 missing_evidence,
38 weak_evidence_references,
39 finding_changes,
40 policy_changes,
41 )
42}
43
44pub fn render_diff_posture_summary_human_with_evidence_health(
45 current_failures: usize,
46 broken_evidence_links: usize,
47 weak_evidence_references: usize,
48 finding_changes: &[DiffFindingChange<'_>],
49 policy_changes: &[DiffPolicyChange<'_>],
50) -> String {
51 render_diff_posture_summary_human_with_evidence_health_counts_inner(
52 current_failures,
53 broken_evidence_links,
54 0,
55 weak_evidence_references,
56 finding_changes,
57 policy_changes,
58 )
59}
60
61fn render_diff_posture_summary_human_with_evidence_health_counts_inner(
62 current_failures: usize,
63 broken_evidence_links: usize,
64 missing_evidence: usize,
65 weak_evidence_references: usize,
66 finding_changes: &[DiffFindingChange<'_>],
67 policy_changes: &[DiffPolicyChange<'_>],
68) -> String {
69 let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
70 let posture = diff_net_posture(summary);
71 let mut out = String::new();
72 out.push_str("\nDiff posture summary:\n");
73 out.push_str(&format!(" net_posture: {}\n", posture.as_str()));
74 out.push_str(&format!(
75 " reviewer_action: {}\n",
76 posture.reviewer_action()
77 ));
78 out.push_str(&format!(
79 " current_check_failures: {}\n",
80 summary.current_failures
81 ));
82 if broken_evidence_links > 0 {
83 out.push_str(&format!(
84 " broken_evidence_links: {broken_evidence_links}\n"
85 ));
86 }
87 if missing_evidence > 0 {
88 out.push_str(&format!(" missing_evidence: {missing_evidence}\n"));
89 }
90 if weak_evidence_references > 0 {
91 out.push_str(&format!(
92 " weak_evidence_references: {weak_evidence_references}\n"
93 ));
94 }
95 let structural_delta = diff_structural_delta_summary(policy_changes);
96 if structural_delta.scope_broadened > 0 {
97 out.push_str(&format!(
98 " scope_broadened: {}\n",
99 structural_delta.scope_broadened
100 ));
101 }
102 if structural_delta.scope_changed > 0 {
103 out.push_str(&format!(
104 " scope_changed: {}\n",
105 structural_delta.scope_changed
106 ));
107 }
108 if structural_delta.scope_narrowed > 0 {
109 out.push_str(&format!(
110 " scope_narrowed: {}\n",
111 structural_delta.scope_narrowed
112 ));
113 }
114 if structural_delta.selector_changed > 0 {
115 out.push_str(&format!(
116 " selector_changed: {}\n",
117 structural_delta.selector_changed
118 ));
119 }
120 if structural_delta.selector_precision_decreased > 0 {
121 out.push_str(&format!(
122 " selector_precision_decreased: {}\n",
123 structural_delta.selector_precision_decreased
124 ));
125 }
126 if structural_delta.selector_precision_increased > 0 {
127 out.push_str(&format!(
128 " selector_precision_increased: {}\n",
129 structural_delta.selector_precision_increased
130 ));
131 }
132 let evidence_delta = diff_evidence_delta_summary(policy_changes);
133 if evidence_delta.evidence_added > 0 {
134 out.push_str(&format!(
135 " evidence_added: {}\n",
136 evidence_delta.evidence_added
137 ));
138 }
139 if evidence_delta.weak_evidence_added > 0 {
140 out.push_str(&format!(
141 " weak_evidence_added: {}\n",
142 evidence_delta.weak_evidence_added
143 ));
144 }
145 if evidence_delta.broken_evidence_added > 0 {
146 out.push_str(&format!(
147 " broken_evidence_added: {}\n",
148 evidence_delta.broken_evidence_added
149 ));
150 }
151 if evidence_delta.evidence_removed > 0 {
152 out.push_str(&format!(
153 " evidence_removed: {}\n",
154 evidence_delta.evidence_removed
155 ));
156 }
157 if evidence_delta.evidence_removal_failures > 0 {
158 out.push_str(&format!(
159 " evidence_removal_failures: {}\n",
160 evidence_delta.evidence_removal_failures
161 ));
162 }
163 if evidence_delta.evidence_removal_review_items > 0 {
164 out.push_str(&format!(
165 " evidence_removal_review_items: {}\n",
166 evidence_delta.evidence_removal_review_items
167 ));
168 }
169 if evidence_delta.evidence_removal_improvements > 0 {
170 out.push_str(&format!(
171 " evidence_removal_improvements: {}\n",
172 evidence_delta.evidence_removal_improvements
173 ));
174 }
175 if evidence_delta.link_added > 0 {
176 out.push_str(&format!(" link_added: {}\n", evidence_delta.link_added));
177 }
178 if evidence_delta.weak_link_added > 0 {
179 out.push_str(&format!(
180 " weak_link_added: {}\n",
181 evidence_delta.weak_link_added
182 ));
183 }
184 if evidence_delta.broken_link_added > 0 {
185 out.push_str(&format!(
186 " broken_link_added: {}\n",
187 evidence_delta.broken_link_added
188 ));
189 }
190 if evidence_delta.link_removed > 0 {
191 out.push_str(&format!(
192 " link_removed: {}\n",
193 evidence_delta.link_removed
194 ));
195 }
196 if evidence_delta.link_removal_failures > 0 {
197 out.push_str(&format!(
198 " link_removal_failures: {}\n",
199 evidence_delta.link_removal_failures
200 ));
201 }
202 if evidence_delta.link_removal_review_items > 0 {
203 out.push_str(&format!(
204 " link_removal_review_items: {}\n",
205 evidence_delta.link_removal_review_items
206 ));
207 }
208 if evidence_delta.link_removal_improvements > 0 {
209 out.push_str(&format!(
210 " link_removal_improvements: {}\n",
211 evidence_delta.link_removal_improvements
212 ));
213 }
214 let evidence_repair_queues = evidence_repair_queues_from_counts(
215 broken_evidence_links,
216 missing_evidence,
217 weak_evidence_references,
218 );
219 if !evidence_repair_queues.is_empty() {
220 out.push_str(" evidence_repair_queues:\n");
221 for queue in evidence_repair_queues {
222 out.push_str(&format!(" {}\n", queue.command));
223 }
224 }
225 out.push_str(&format!(
226 " new_source_findings: {}\n",
227 summary.new_findings
228 ));
229 out.push_str(&format!(
230 " removed_source_findings: {}\n",
231 summary.removed_findings
232 ));
233 out.push_str(&format!(" policy_failures: {}\n", summary.policy_failures));
234 out.push_str(&format!(
235 " policy_review_items: {}\n",
236 summary.policy_review_items
237 ));
238 out.push_str(&format!(
239 " policy_improvements: {}\n",
240 summary.policy_improvements
241 ));
242 out
243}
244
245pub fn render_diff_finding_changes_human(changes: &[DiffFindingChange<'_>]) -> String {
246 let mut out = String::new();
247 out.push_str("\nFinding posture changes:\n");
248 if changes.is_empty() {
249 out.push_str(" none\n");
250 return out;
251 }
252 append_finding_changes_human_section(&mut out, "Finding attention", changes, "new");
253 append_finding_changes_human_section(&mut out, "Finding improvements", changes, "removed");
254 let known_changes = ["new", "removed"];
255 if changes
256 .iter()
257 .any(|change| !known_changes.contains(&change.change))
258 {
259 out.push_str(" Other finding changes:\n");
260 let other_count = changes
261 .iter()
262 .filter(|change| !known_changes.contains(&change.change))
263 .count();
264 for change in changes
265 .iter()
266 .filter(|change| !known_changes.contains(&change.change))
267 .take(DIFF_HUMAN_CHANGE_LIMIT)
268 {
269 append_finding_change_human_row(&mut out, change);
270 }
271 if other_count > DIFF_HUMAN_CHANGE_LIMIT {
272 out.push_str(&format!(
273 " ... {} more omitted\n",
274 other_count - DIFF_HUMAN_CHANGE_LIMIT
275 ));
276 }
277 }
278 out
279}
280
281fn append_finding_changes_human_section(
282 out: &mut String,
283 heading: &str,
284 changes: &[DiffFindingChange<'_>],
285 change_kind: &str,
286) {
287 if !changes.iter().any(|change| change.change == change_kind) {
288 return;
289 }
290 out.push_str(&format!(" {heading}:\n"));
291 let matching_count = changes
292 .iter()
293 .filter(|change| change.change == change_kind)
294 .count();
295 for change in changes
296 .iter()
297 .filter(|change| change.change == change_kind)
298 .take(DIFF_HUMAN_CHANGE_LIMIT)
299 {
300 append_finding_change_human_row(out, change);
301 }
302 if matching_count > DIFF_HUMAN_CHANGE_LIMIT {
303 out.push_str(&format!(
304 " ... {} more omitted\n",
305 matching_count - DIFF_HUMAN_CHANGE_LIMIT
306 ));
307 }
308}
309
310fn append_finding_change_human_row(out: &mut String, change: &DiffFindingChange<'_>) {
311 let source_package = change
312 .source_package
313 .map(|package| format!(" source_package={package}"))
314 .unwrap_or_default();
315 let identity = change
316 .identity
317 .map(|identity| format!(" identity={}", structural_identity_summary(identity)))
318 .unwrap_or_default();
319 out.push_str(&format!(
320 " {} {}{} at {}{}{}\n",
321 change.change,
322 change.kind,
323 change
324 .family
325 .map(|family| format!(".{family}"))
326 .unwrap_or_default(),
327 finding_location(change),
328 source_package,
329 identity
330 ));
331}
332
333fn finding_location(change: &DiffFindingChange<'_>) -> String {
334 match (change.line, change.column) {
335 (Some(line), Some(column)) => format!("{}:{line}:{column}", change.path),
336 (Some(line), None) => format!("{}:{line}", change.path),
337 (None, Some(column)) => format!("{} column={column}", change.path),
338 (None, None) => change.path.to_string(),
339 }
340}
341
342pub fn render_diff_policy_changes_human(changes: &[DiffPolicyChange<'_>]) -> String {
343 let mut out = String::new();
344 out.push_str("\nPolicy posture changes:\n");
345 if changes.is_empty() {
346 out.push_str(" none\n");
347 return out;
348 }
349 append_policy_changes_human_section(&mut out, "Policy failures", changes, "fail");
350 append_policy_changes_human_section(&mut out, "Policy review required", changes, "review");
351 append_policy_changes_human_section(&mut out, "Policy improvements", changes, "improvement");
352 let known_severities = ["fail", "review", "improvement"];
353 if changes
354 .iter()
355 .any(|change| !known_severities.contains(&change.severity))
356 {
357 out.push_str(" Other policy changes:\n");
358 for change in changes
359 .iter()
360 .filter(|change| !known_severities.contains(&change.severity))
361 {
362 append_policy_change_human_row(&mut out, change);
363 }
364 }
365 out
366}
367
368fn append_policy_changes_human_section(
369 out: &mut String,
370 heading: &str,
371 changes: &[DiffPolicyChange<'_>],
372 severity: &str,
373) {
374 if !changes.iter().any(|change| change.severity == severity) {
375 return;
376 }
377 out.push_str(&format!(" {heading}:\n"));
378 for change in changes {
379 if change.severity == severity {
380 append_policy_change_human_row(out, change);
381 }
382 }
383}
384
385fn append_policy_change_human_row(out: &mut String, change: &DiffPolicyChange<'_>) {
386 out.push_str(&format!(
387 " {} {} {}: {}\n",
388 change.severity, change.allow_id, change.kind, change.message
389 ));
390 if let Some(detail) = policy_change_detail(change) {
391 out.push_str(&format!(" detail: {detail}\n"));
392 }
393}