use crate::code_audit::{
baseline, AuditFinding, CodeAuditResult, ConventionReport, DirectoryConvention, Severity,
};
use crate::refactor::{
auto::{FixResult, FixResultsSummary, PolicySummary},
AuditRefactorIterationSummary,
};
use serde::Serialize;
use super::run::AuditRunWorkflowResult;
#[derive(Serialize)]
pub struct AuditSummaryOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub alignment_score: Option<f32>,
pub total_findings: usize,
pub warnings: usize,
pub info: usize,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub top_findings: Vec<AuditSummaryFinding>,
pub exit_code: i32,
}
#[derive(Serialize)]
pub struct AuditSummaryFinding {
pub file: String,
pub convention: String,
pub kind: AuditFinding,
pub severity: Severity,
pub description: String,
pub suggestion: String,
}
#[derive(Serialize)]
#[serde(tag = "command")]
pub enum AuditCommandOutput {
#[serde(rename = "audit")]
Full(CodeAuditResult),
#[serde(rename = "audit.conventions")]
Conventions {
component_id: String,
conventions: Vec<ConventionReport>,
#[serde(skip_serializing_if = "Vec::is_empty")]
directory_conventions: Vec<DirectoryConvention>,
},
#[serde(rename = "audit.fix")]
Fix {
component_id: String,
source_path: String,
status: String,
#[serde(flatten)]
fix_result: FixResult,
#[serde(skip_serializing_if = "Option::is_none")]
fix_summary: Option<FixResultsSummary>,
policy_summary: AuditFixPolicySummary,
#[serde(skip_serializing_if = "Vec::is_empty")]
iterations: Vec<AuditFixIterationSummary>,
written: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
hints: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ratchet_summary: Option<AutoRatchetSummary>,
},
#[serde(rename = "audit.baseline")]
BaselineSaved {
component_id: String,
path: String,
findings_count: usize,
outliers_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
alignment_score: Option<f32>,
},
#[serde(rename = "audit.compared")]
Compared {
#[serde(flatten)]
result: CodeAuditResult,
baseline_comparison: baseline::BaselineComparison,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<AuditSummaryOutput>,
},
#[serde(rename = "audit.summary")]
Summary(AuditSummaryOutput),
}
#[derive(Debug, Serialize)]
pub struct AutoRatchetSummary {
pub resolved_count: usize,
pub previous_count: usize,
pub current_count: usize,
pub baseline_updated: bool,
}
#[derive(Debug, Serialize)]
pub struct AuditFixPolicySummary {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub selected_only: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub excluded: Vec<String>,
pub visible_insertions: usize,
pub visible_new_files: usize,
pub auto_apply_insertions: usize,
pub auto_apply_new_files: usize,
pub blocked_insertions: usize,
pub blocked_new_files: usize,
pub preflight_failures: usize,
}
pub type AuditFixIterationSummary = AuditRefactorIterationSummary;
pub fn build_audit_summary(result: &CodeAuditResult, exit_code: i32) -> AuditSummaryOutput {
let warnings = result
.findings
.iter()
.filter(|f| matches!(f.severity, Severity::Warning))
.count();
let info = result
.findings
.iter()
.filter(|f| matches!(f.severity, Severity::Info))
.count();
let top_findings = result
.findings
.iter()
.take(20)
.map(|f| AuditSummaryFinding {
file: f.file.clone(),
convention: f.convention.clone(),
kind: f.kind.clone(),
severity: f.severity.clone(),
description: f.description.clone(),
suggestion: f.suggestion.clone(),
})
.collect();
AuditSummaryOutput {
alignment_score: result.summary.alignment_score,
total_findings: result.findings.len(),
warnings,
info,
top_findings,
exit_code,
}
}
pub fn build_fix_policy_summary(
policy: &PolicySummary,
only: Vec<String>,
excluded: Vec<String>,
) -> AuditFixPolicySummary {
AuditFixPolicySummary {
selected_only: only,
excluded,
visible_insertions: policy.visible_insertions,
visible_new_files: policy.visible_new_files,
auto_apply_insertions: policy.auto_apply_insertions,
auto_apply_new_files: policy.auto_apply_new_files,
blocked_insertions: policy.blocked_insertions,
blocked_new_files: policy.blocked_new_files,
preflight_failures: policy.preflight_failures,
}
}
pub fn build_fix_hints(written: bool, summary: &PolicySummary) -> Vec<String> {
let mut hints = Vec::new();
if !written && summary.has_blocked_items() {
hints.push(format!(
"{} fix(es) are visible but would be blocked on --write because they are safe_with_checks or plan_only.",
summary.blocked_insertions + summary.blocked_new_files
));
}
if summary.preflight_failures > 0 {
hints.push(format!(
"{} fix(es) failed deterministic preflight checks and will stay preview-only until their validator passes.",
summary.preflight_failures
));
}
if written && summary.has_blocked_items() {
hints.push(format!(
"Applied only safe_auto fixes. {} fix(es) were left as preview because they need checks or manual review.",
summary.blocked_insertions + summary.blocked_new_files
));
}
hints
}
pub fn log_fix_summary(result: &FixResult, policy: &PolicySummary, written: bool) {
let kind_counts = result.finding_counts();
let total_insertions = result.total_insertions;
let total_new_files = result.new_files.len();
let total_skipped = result.skipped.len();
if total_insertions == 0 && total_new_files == 0 {
crate::log_status!("fix", "No fixes to apply");
return;
}
let mode = if written { "Applied" } else { "Would apply" };
crate::log_status!(
"fix",
"{mode} {total_insertions} insertion(s) across {} file(s), {} new file(s)",
result.files_modified,
total_new_files
);
for (kind, count) in &kind_counts {
crate::log_status!("fix", " {kind:?}: {count}");
}
if total_skipped > 0 {
crate::log_status!("fix", "Skipped: {total_skipped} file(s)");
}
if policy.has_blocked_items() {
crate::log_status!(
"fix",
"Blocked: {} insertion(s), {} new file(s) (safe_with_checks or plan_only)",
policy.blocked_insertions,
policy.blocked_new_files
);
}
if policy.preflight_failures > 0 {
crate::log_status!("fix", "Preflight failures: {}", policy.preflight_failures);
}
}
pub fn from_main_workflow(result: AuditRunWorkflowResult) -> (AuditCommandOutput, i32) {
let exit_code = result.exit_code;
(result.output, exit_code)
}
#[cfg(test)]
#[path = "../../../tests/core/code_audit/report_test.rs"]
mod report_test;