use crate::diff_policy_detail::policy_change_detail;
use crate::diff_posture::{diff_net_posture, diff_posture_summary};
use crate::text::markdown_cell;
use crate::{CLAIM_BOUNDARY_TEXT, DiffFindingChange, DiffPolicyChange};
const PR_SUMMARY_HIGHLIGHT_LIMIT: usize = 8;
pub fn render_diff_pr_summary_markdown(
current_failures: usize,
finding_changes: &[DiffFindingChange<'_>],
policy_changes: &[DiffPolicyChange<'_>],
) -> String {
render_diff_pr_summary_markdown_with_evidence_health(
current_failures,
0,
0,
finding_changes,
policy_changes,
)
}
pub fn render_diff_pr_summary_markdown_with_evidence_health(
current_failures: usize,
broken_evidence_links: usize,
weak_evidence_references: usize,
finding_changes: &[DiffFindingChange<'_>],
policy_changes: &[DiffPolicyChange<'_>],
) -> String {
let summary = diff_posture_summary(current_failures, finding_changes, policy_changes);
let posture = diff_net_posture(summary);
let mut out = String::new();
out.push_str("## PR Summary\n\n");
out.push_str(&format!("**Net posture:** `{}`\n\n", posture.as_str()));
out.push_str("| Signal | Count |\n|---|---:|\n");
out.push_str(&format!(
"| Current check failures | {} |\n",
summary.current_failures
));
if broken_evidence_links > 0 {
out.push_str(&format!(
"| Broken evidence links | {broken_evidence_links} |\n"
));
}
if weak_evidence_references > 0 {
out.push_str(&format!(
"| Weak evidence/link references | {weak_evidence_references} |\n"
));
}
out.push_str(&format!(
"| New source findings | {} |\n",
summary.new_findings
));
out.push_str(&format!(
"| Removed source findings | {} |\n",
summary.removed_findings
));
out.push_str(&format!(
"| Policy failures | {} |\n",
summary.policy_failures
));
out.push_str(&format!(
"| Policy review items | {} |\n",
summary.policy_review_items
));
out.push_str(&format!(
"| Policy improvements | {} |\n",
summary.policy_improvements
));
out.push_str(&format!(
"\n**Reviewer action:** {}\n\n",
posture.reviewer_action()
));
out.push_str("> ");
out.push_str(CLAIM_BOUNDARY_TEXT);
out.push_str("\n\n");
append_finding_highlights(&mut out, finding_changes);
append_policy_highlights(&mut out, policy_changes);
out
}
fn append_finding_highlights(out: &mut String, finding_changes: &[DiffFindingChange<'_>]) {
let new_count = finding_changes
.iter()
.filter(|change| change.change == "new")
.count();
if new_count > 0 {
out.push_str("### Finding Attention\n\n");
out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
for change in finding_changes
.iter()
.filter(|change| change.change == "new")
.take(PR_SUMMARY_HIGHLIGHT_LIMIT)
{
append_finding_highlight_row(out, change);
}
append_omitted_summary_note(out, new_count, "new finding change");
out.push('\n');
}
let removed_count = finding_changes
.iter()
.filter(|change| change.change == "removed")
.count();
if removed_count > 0 {
out.push_str("### Finding Improvements\n\n");
out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
for change in finding_changes
.iter()
.filter(|change| change.change == "removed")
.take(PR_SUMMARY_HIGHLIGHT_LIMIT)
{
append_finding_highlight_row(out, change);
}
append_omitted_summary_note(out, removed_count, "removed finding change");
out.push('\n');
}
}
fn append_finding_highlight_row(out: &mut String, change: &DiffFindingChange<'_>) {
out.push_str(&format!(
"| `{}` | `{}` | `{}` | `{}` |\n",
markdown_cell(change.change),
markdown_cell(change.kind),
markdown_cell(change.family.unwrap_or("")),
markdown_cell(change.path)
));
}
fn append_policy_highlights(out: &mut String, policy_changes: &[DiffPolicyChange<'_>]) {
let attention_count = policy_changes
.iter()
.filter(|change| change.severity != "improvement")
.count();
if attention_count > 0 {
out.push_str("### Policy Attention\n\n");
out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
for change in policy_changes
.iter()
.filter(|change| change.severity != "improvement")
.take(PR_SUMMARY_HIGHLIGHT_LIMIT)
{
append_policy_highlight_row(out, change);
}
append_omitted_summary_note(out, attention_count, "policy attention change");
out.push('\n');
}
let improvement_count = policy_changes
.iter()
.filter(|change| change.severity == "improvement")
.count();
if improvement_count > 0 {
out.push_str("### Policy Improvements\n\n");
out.push_str("| Allow ID | Kind | Detail | Message |\n|---|---|---|---|\n");
for change in policy_changes
.iter()
.filter(|change| change.severity == "improvement")
.take(PR_SUMMARY_HIGHLIGHT_LIMIT)
{
let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
out.push_str(&format!(
"| `{}` | `{}` | {} | {} |\n",
markdown_cell(change.allow_id),
markdown_cell(change.kind),
markdown_cell(&detail),
markdown_cell(change.message)
));
}
append_omitted_summary_note(out, improvement_count, "policy improvement change");
out.push('\n');
}
}
fn append_omitted_summary_note(out: &mut String, count: usize, singular_label: &str) {
if count > PR_SUMMARY_HIGHLIGHT_LIMIT {
let omitted = count - PR_SUMMARY_HIGHLIGHT_LIMIT;
let plural = if omitted == 1 { "" } else { "s" };
out.push_str(&format!(
"\n{omitted} additional {singular_label}{plural} omitted from this summary.\n"
));
}
}
fn append_policy_highlight_row(out: &mut String, change: &DiffPolicyChange<'_>) {
let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
out.push_str(&format!(
"| `{}` | `{}` | `{}` | {} | {} |\n",
markdown_cell(change.severity),
markdown_cell(change.allow_id),
markdown_cell(change.kind),
markdown_cell(&detail),
markdown_cell(change.message)
));
}
pub fn insert_markdown_pr_summary(text: &mut String, summary: &str) {
let marker = "Findings scanned:";
if let Some(index) = text.find(marker) {
text.insert_str(index, summary);
} else {
text.push('\n');
text.push_str(summary);
}
}
pub fn render_diff_finding_changes_markdown(changes: &[DiffFindingChange<'_>]) -> String {
let mut out = String::new();
out.push_str("\n## Finding Posture Changes\n\n");
if changes.is_empty() {
out.push_str("No source finding posture changes detected.\n");
return out;
}
out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
for change in changes.iter().take(120) {
out.push_str(&format!(
"| `{}` | `{}` | `{}` | `{}` |\n",
markdown_cell(change.change),
markdown_cell(change.kind),
markdown_cell(change.family.unwrap_or("")),
markdown_cell(change.path)
));
}
if changes.len() > 120 {
out.push_str(&format!(
"\n{} additional finding posture changes omitted.\n",
changes.len() - 120
));
}
out
}
pub fn render_diff_policy_changes_markdown(changes: &[DiffPolicyChange<'_>]) -> String {
let mut out = String::new();
out.push_str("\n## Policy Posture Changes\n\n");
if changes.is_empty() {
out.push_str("No policy weakening detected.\n");
return out;
}
out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
for change in changes {
let detail = policy_change_detail(change).unwrap_or_else(|| "none".to_string());
out.push_str(&format!(
"| `{}` | `{}` | `{}` | {} | {} |\n",
markdown_cell(change.severity),
markdown_cell(change.allow_id),
markdown_cell(change.kind),
markdown_cell(&detail),
markdown_cell(change.message)
));
}
out
}