use crate::diff_policy_detail::policy_change_detail;
use crate::diff_posture::{diff_net_posture, diff_posture_summary};
use crate::evidence_repair::evidence_repair_queues_from_counts;
use crate::text::markdown_cell;
use crate::{CLAIM_BOUNDARY_TEXT, DiffFindingChange, DiffPolicyChange};
const PR_SUMMARY_HIGHLIGHT_LIMIT: usize = 8;
const DIFF_MARKDOWN_CHANGE_LIMIT: usize = 120;
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_counts(
current_failures: usize,
broken_evidence_links: usize,
missing_evidence: usize,
weak_evidence_references: usize,
finding_changes: &[DiffFindingChange<'_>],
policy_changes: &[DiffPolicyChange<'_>],
) -> String {
render_diff_pr_summary_markdown_with_evidence_health_counts_inner(
current_failures,
broken_evidence_links,
missing_evidence,
weak_evidence_references,
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 {
render_diff_pr_summary_markdown_with_evidence_health_counts_inner(
current_failures,
broken_evidence_links,
0,
weak_evidence_references,
finding_changes,
policy_changes,
)
}
fn render_diff_pr_summary_markdown_with_evidence_health_counts_inner(
current_failures: usize,
broken_evidence_links: usize,
missing_evidence: 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 missing_evidence > 0 {
out.push_str(&format!("| Missing evidence | {missing_evidence} |\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()
));
let evidence_repair_queues = evidence_repair_queues_from_counts(
broken_evidence_links,
missing_evidence,
weak_evidence_references,
);
if !evidence_repair_queues.is_empty() {
out.push_str("**Evidence repair queues:**\n");
for queue in evidence_repair_queues {
out.push_str(&format!("- `{}`\n", queue.command));
}
out.push('\n');
}
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<'_>]) {
append_policy_severity_highlights(
out,
policy_changes,
"fail",
"### Policy Failures",
"policy failure",
);
append_policy_severity_highlights(
out,
policy_changes,
"review",
"### Policy Review Required",
"policy review item",
);
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_policy_severity_highlights(
out: &mut String,
policy_changes: &[DiffPolicyChange<'_>],
severity: &str,
heading: &str,
singular_label: &str,
) {
let count = policy_changes
.iter()
.filter(|change| change.severity == severity)
.count();
if count == 0 {
return;
}
out.push_str(heading);
out.push_str("\n\n");
out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
for change in policy_changes
.iter()
.filter(|change| change.severity == severity)
.take(PR_SUMMARY_HIGHLIGHT_LIMIT)
{
append_policy_highlight_row(out, change);
}
append_omitted_summary_note(out, count, singular_label);
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;
}
append_finding_changes_markdown_section(&mut out, "Finding Attention", changes, "new");
append_finding_changes_markdown_section(&mut out, "Finding Improvements", changes, "removed");
let known_changes = ["new", "removed"];
if changes
.iter()
.any(|change| !known_changes.contains(&change.change))
{
out.push_str("### Other Finding Changes\n\n");
append_finding_changes_markdown_table(
&mut out,
changes
.iter()
.filter(|change| !known_changes.contains(&change.change)),
);
}
out
}
fn append_finding_changes_markdown_section<'a>(
out: &mut String,
heading: &str,
changes: &'a [DiffFindingChange<'a>],
change_kind: &str,
) {
if !changes.iter().any(|change| change.change == change_kind) {
return;
}
out.push_str(&format!("### {heading}\n\n"));
append_finding_changes_markdown_table(
out,
changes.iter().filter(|change| change.change == change_kind),
);
}
fn append_finding_changes_markdown_table<'a>(
out: &mut String,
changes: impl Iterator<Item = &'a DiffFindingChange<'a>>,
) {
let changes = changes.collect::<Vec<_>>();
out.push_str("| Change | Kind | Family | Path |\n|---|---|---|---|\n");
for change in changes.iter().take(DIFF_MARKDOWN_CHANGE_LIMIT) {
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() > DIFF_MARKDOWN_CHANGE_LIMIT {
out.push_str(&format!(
"\n{} additional finding posture changes omitted.\n",
changes.len() - DIFF_MARKDOWN_CHANGE_LIMIT
));
}
out.push('\n');
}
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;
}
append_policy_changes_markdown_section(&mut out, "Policy Failures", changes, "fail");
append_policy_changes_markdown_section(&mut out, "Policy Review Required", changes, "review");
append_policy_changes_markdown_section(&mut out, "Policy Improvements", changes, "improvement");
let known_severities = ["fail", "review", "improvement"];
if changes
.iter()
.any(|change| !known_severities.contains(&change.severity))
{
out.push_str("### Other Policy Changes\n\n");
append_policy_changes_markdown_table(
&mut out,
changes
.iter()
.filter(|change| !known_severities.contains(&change.severity)),
);
}
out
}
fn append_policy_changes_markdown_section<'a>(
out: &mut String,
heading: &str,
changes: &'a [DiffPolicyChange<'a>],
severity: &str,
) {
if !changes.iter().any(|change| change.severity == severity) {
return;
}
out.push_str(&format!("### {heading}\n\n"));
append_policy_changes_markdown_table(
out,
changes.iter().filter(|change| change.severity == severity),
);
}
fn append_policy_changes_markdown_table<'a>(
out: &mut String,
changes: impl Iterator<Item = &'a DiffPolicyChange<'a>>,
) {
let changes = changes.collect::<Vec<_>>();
out.push_str("| Severity | Allow ID | Kind | Detail | Message |\n|---|---|---|---|---|\n");
for change in changes.iter().take(DIFF_MARKDOWN_CHANGE_LIMIT) {
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)
));
}
if changes.len() > DIFF_MARKDOWN_CHANGE_LIMIT {
out.push_str(&format!(
"\n{} additional policy posture changes omitted.\n",
changes.len() - DIFF_MARKDOWN_CHANGE_LIMIT
));
}
out.push('\n');
}