allow_report/
diff_markdown.rs1use crate::diff_policy_detail::policy_change_detail;
2use crate::diff_posture::{diff_net_posture, diff_posture_summary};
3use crate::text::markdown_cell;
4use crate::{CLAIM_BOUNDARY_TEXT, DiffFindingChange, DiffPolicyChange};
5
6const PR_SUMMARY_HIGHLIGHT_LIMIT: usize = 8;
7
8pub fn render_diff_pr_summary_markdown(
9 current_failures: usize,
10 finding_changes: &[DiffFindingChange<'_>],
11 policy_changes: &[DiffPolicyChange<'_>],
12) -> String {
13 render_diff_pr_summary_markdown_with_evidence_health(
14 current_failures,
15 0,
16 0,
17 finding_changes,
18 policy_changes,
19 )
20}
21
22pub fn render_diff_pr_summary_markdown_with_evidence_health(
23 current_failures: usize,
24 broken_evidence_links: usize,
25 weak_evidence_references: usize,
26 finding_changes: &[DiffFindingChange<'_>],
27 policy_changes: &[DiffPolicyChange<'_>],
28) -> String {
29 let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
30 let posture = diff_net_posture(summary);
31 let mut out = String::new();
32 out.push_str("## PR Summary\n\n");
33 out.push_str(&format!("**Net posture:** `{}`\n\n", posture.as_str()));
34 out.push_str("| Signal | Count |\n|---|---:|\n");
35 out.push_str(&format!(
36 "| Current check failures | {} |\n",
37 summary.current_failures
38 ));
39 if broken_evidence_links > 0 {
40 out.push_str(&format!(
41 "| Broken evidence links | {broken_evidence_links} |\n"
42 ));
43 }
44 if weak_evidence_references > 0 {
45 out.push_str(&format!(
46 "| Weak evidence/link references | {weak_evidence_references} |\n"
47 ));
48 }
49 out.push_str(&format!(
50 "| New source findings | {} |\n",
51 summary.new_findings
52 ));
53 out.push_str(&format!(
54 "| Removed source findings | {} |\n",
55 summary.removed_findings
56 ));
57 out.push_str(&format!(
58 "| Policy failures | {} |\n",
59 summary.policy_failures
60 ));
61 out.push_str(&format!(
62 "| Policy review items | {} |\n",
63 summary.policy_review_items
64 ));
65 out.push_str(&format!(
66 "| Policy improvements | {} |\n",
67 summary.policy_improvements
68 ));
69 out.push_str(&format!(
70 "\n**Reviewer action:** {}\n\n",
71 posture.reviewer_action()
72 ));
73 out.push_str("> ");
74 out.push_str(CLAIM_BOUNDARY_TEXT);
75 out.push_str("\n\n");
76 append_finding_highlights(&mut out, finding_changes);
77 append_policy_highlights(&mut out, policy_changes);
78 out
79}
80
81fn append_finding_highlights(out: &mut String, finding_changes: &[DiffFindingChange<'_>]) {
82 let new_count = finding_changes
83 .iter()
84 .filter(|change| change.change == "new")
85 .count();
86 if new_count > 0 {
87 out.push_str("### Finding Attention\n\n");
88 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
89 for change in finding_changes
90 .iter()
91 .filter(|change| change.change == "new")
92 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
93 {
94 append_finding_highlight_row(out, change);
95 }
96 append_omitted_summary_note(out, new_count, "new finding change");
97 out.push('\n');
98 }
99
100 let removed_count = finding_changes
101 .iter()
102 .filter(|change| change.change == "removed")
103 .count();
104 if removed_count > 0 {
105 out.push_str("### Finding Improvements\n\n");
106 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
107 for change in finding_changes
108 .iter()
109 .filter(|change| change.change == "removed")
110 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
111 {
112 append_finding_highlight_row(out, change);
113 }
114 append_omitted_summary_note(out, removed_count, "removed finding change");
115 out.push('\n');
116 }
117}
118
119fn append_finding_highlight_row(out: &mut String, change: &DiffFindingChange<'_>) {
120 out.push_str(&format!(
121 "| `{}` | `{}` | `{}` | `{}` |\n",
122 markdown_cell(change.change),
123 markdown_cell(change.kind),
124 markdown_cell(change.family.unwrap_or("")),
125 markdown_cell(change.path)
126 ));
127}
128
129fn append_policy_highlights(out: &mut String, policy_changes: &[DiffPolicyChange<'_>]) {
130 let attention_count = policy_changes
131 .iter()
132 .filter(|change| change.severity != "improvement")
133 .count();
134 if attention_count > 0 {
135 out.push_str("### Policy Attention\n\n");
136 out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
137 for change in policy_changes
138 .iter()
139 .filter(|change| change.severity != "improvement")
140 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
141 {
142 append_policy_highlight_row(out, change);
143 }
144 append_omitted_summary_note(out, attention_count, "policy attention change");
145 out.push('\n');
146 }
147
148 let improvement_count = policy_changes
149 .iter()
150 .filter(|change| change.severity == "improvement")
151 .count();
152 if improvement_count > 0 {
153 out.push_str("### Policy Improvements\n\n");
154 out.push_str("| Allow ID | Kind | Detail | Message |\n|---|---|---|---|\n");
155 for change in policy_changes
156 .iter()
157 .filter(|change| change.severity == "improvement")
158 .take(PR_SUMMARY_HIGHLIGHT_LIMIT)
159 {
160 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
161 out.push_str(&format!(
162 "| `{}` | `{}` | {} | {} |\n",
163 markdown_cell(change.allow_id),
164 markdown_cell(change.kind),
165 markdown_cell(&detail),
166 markdown_cell(change.message)
167 ));
168 }
169 append_omitted_summary_note(out, improvement_count, "policy improvement change");
170 out.push('\n');
171 }
172}
173
174fn append_omitted_summary_note(out: &mut String, count: usize, singular_label: &str) {
175 if count > PR_SUMMARY_HIGHLIGHT_LIMIT {
176 let omitted = count - PR_SUMMARY_HIGHLIGHT_LIMIT;
177 let plural = if omitted == 1 { "" } else { "s" };
178 out.push_str(&format!(
179 "\n{omitted} additional {singular_label}{plural} omitted from this summary.\n"
180 ));
181 }
182}
183
184fn append_policy_highlight_row(out: &mut String, change: &DiffPolicyChange<'_>) {
185 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
186 out.push_str(&format!(
187 "| `{}` | `{}` | `{}` | {} | {} |\n",
188 markdown_cell(change.severity),
189 markdown_cell(change.allow_id),
190 markdown_cell(change.kind),
191 markdown_cell(&detail),
192 markdown_cell(change.message)
193 ));
194}
195
196pub fn insert_markdown_pr_summary(text: &mut String, summary: &str) {
197 let marker = "Findings scanned:";
198 if let Some(index) = text.find(marker) {
199 text.insert_str(index, summary);
200 } else {
201 text.push('\n');
202 text.push_str(summary);
203 }
204}
205
206pub fn render_diff_finding_changes_markdown(changes: &[DiffFindingChange<'_>]) -> String {
207 let mut out = String::new();
208 out.push_str("\n## Finding Posture Changes\n\n");
209 if changes.is_empty() {
210 out.push_str("No source finding posture changes detected.\n");
211 return out;
212 }
213 out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
214 for change in changes.iter().take(120) {
215 out.push_str(&format!(
216 "| `{}` | `{}` | `{}` | `{}` |\n",
217 markdown_cell(change.change),
218 markdown_cell(change.kind),
219 markdown_cell(change.family.unwrap_or("")),
220 markdown_cell(change.path)
221 ));
222 }
223 if changes.len() > 120 {
224 out.push_str(&format!(
225 "\n{} additional finding posture changes omitted.\n",
226 changes.len() - 120
227 ));
228 }
229 out
230}
231
232pub fn render_diff_policy_changes_markdown(changes: &[DiffPolicyChange<'_>]) -> String {
233 let mut out = String::new();
234 out.push_str("\n## Policy Posture Changes\n\n");
235 if changes.is_empty() {
236 out.push_str("No policy weakening detected.\n");
237 return out;
238 }
239 out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
240 for change in changes {
241 let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
242 out.push_str(&format!(
243 "| `{}` | `{}` | `{}` | {} | {} |\n",
244 markdown_cell(change.severity),
245 markdown_cell(change.allow_id),
246 markdown_cell(change.kind),
247 markdown_cell(&detail),
248 markdown_cell(change.message)
249 ));
250 }
251 out
252}