1use crate::analysis::state::Confidence;
3use crate::report::violations::{Violation, ViolationTier};
4use comfy_table::Table;
5use owo_colors::{OwoColorize, Style};
6
7#[derive(Debug, PartialEq, Eq)]
9pub enum Verdict {
10 Halt, Cautious, SafeWithRisk, Safe, }
15
16impl Verdict {
17 pub fn label(&self) -> &'static str {
18 match self {
19 Verdict::Halt => "HALT",
20 Verdict::Cautious => "CAUTIOUS",
21 Verdict::SafeWithRisk => "SAFE WITH RISK",
22 Verdict::Safe => "SAFE",
23 }
24 }
25
26 pub fn recommendation(&self) -> &'static str {
27 match self {
28 Verdict::Halt => "do not deploy",
29 Verdict::Cautious => "review warnings before deploy",
30 Verdict::SafeWithRisk => "irreversible operations present — ensure backups exist",
31 Verdict::Safe => "safe to deploy",
32 }
33 }
34}
35
36pub fn compute_verdict(violations: &[Violation]) -> Verdict {
38 let has_tier1 = violations.iter().any(|v| v.tier == ViolationTier::Tier1);
39 let has_tier2 = violations.iter().any(|v| v.tier == ViolationTier::Tier2);
40 let has_irreversible_tier3 = violations
41 .iter()
42 .any(|v| v.tier == ViolationTier::Tier3 && v.rule_id == "irreversible-migration");
43
44 match (has_tier1, has_tier2, has_irreversible_tier3) {
45 (true, _, _) => Verdict::Halt,
46 (false, true, _) => Verdict::Cautious,
47 (false, false, true) => Verdict::SafeWithRisk,
48 (false, false, false) => Verdict::Safe,
49 }
50}
51
52fn no_color() -> bool {
53 std::env::var("NO_COLOR").is_ok()
54}
55pub(crate) fn tier_label_colored(tier: &ViolationTier) -> String {
56 let label = match tier {
57 ViolationTier::Tier1 => "HALT",
58 ViolationTier::Tier2 => "WARN",
59 ViolationTier::Tier3 => "SAFE",
60 };
61 if no_color() {
62 label.to_string()
63 } else {
64 match tier {
65 ViolationTier::Tier1 => label.style(Style::new().red().bold()).to_string(),
66 ViolationTier::Tier2 => label.style(Style::new().yellow().bold()).to_string(),
67 ViolationTier::Tier3 => label.style(Style::new().green().bold()).to_string(),
68 }
69 }
70}
71
72fn terminal_width() -> usize {
73 terminal_size::terminal_size()
74 .map(|(w, _)| w.0 as usize)
75 .unwrap_or(80)
76 .max(60)
77}
78
79pub struct Reporter;
80
81impl Reporter {
82 pub fn print_json_report(violations: &[Violation], confidence: &Confidence) {
83 let verdict = compute_verdict(violations);
84 let output = serde_json::json!({
85 "confidence": match confidence {
86 Confidence::Exact => "Exact",
87 Confidence::Tainted => "Tainted",
88 },
89 "verdict": verdict.label(),
90 "violations": violations,
91 });
92 println!("{}", serde_json::to_string_pretty(&output).unwrap());
93 }
94
95 pub fn print_report(violations: &[Violation], confidence: &Confidence) -> bool {
96 let mut tier1 = 0usize;
97 let mut tier2 = 0usize;
98 let mut tier3 = 0usize;
99
100 for v in violations {
101 match v.tier {
102 ViolationTier::Tier1 => tier1 += 1,
103 ViolationTier::Tier2 => tier2 += 1,
104 ViolationTier::Tier3 => tier3 += 1,
105 }
106 }
107
108 let verdict = compute_verdict(violations);
109 let conf_str = match confidence {
110 Confidence::Exact => "Exact",
111 Confidence::Tainted => "Tainted",
112 };
113
114 let width = terminal_width();
115
116 let mut header_table = Table::new();
118 header_table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
119 header_table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth);
120 header_table.set_width(width as u16);
121 header_table.set_header(vec!["safe-migrate lint"]);
122 header_table.add_row(vec![format!(
123 "Verdict: {} Confidence: {}",
124 verdict.label(),
125 conf_str
126 )]);
127 header_table.add_row(vec![format!(
128 "HALT: {} WARN: {} SAFE: {}",
129 tier1, tier2, tier3
130 )]);
131 println!("{}", header_table);
132
133 if violations.is_empty() {
134 println!("\n No violations detected.\n");
135 return false;
136 }
137
138 println!();
139
140 let sep_width = (width as f32 * 0.82) as usize;
142
143 let mut groups: Vec<(usize, Vec<usize>)> = Vec::new();
146 let mut sql_to_group_idx: std::collections::HashMap<(&str, &str), usize> =
147 std::collections::HashMap::new();
148
149 for (i, v) in violations.iter().enumerate() {
150 if let Some(sql) = &v.sql {
151 let key = (sql.as_str(), v.object_name.as_str());
152 if let Some(&gi) = sql_to_group_idx.get(&key) {
153 groups[gi].1.push(i);
154 continue;
155 }
156
157 let new_gi = groups.len();
158 groups.push((i, Vec::new()));
159 sql_to_group_idx.insert(key, new_gi);
160 } else {
161 groups.push((i, Vec::new()));
163 }
164 }
165
166 for (gi, (primary_idx, secondary_idxs)) in groups.iter().enumerate() {
167 let v = &violations[*primary_idx];
168 let tier_str = tier_label_colored(&v.tier);
169
170 println!(" [{}] {}", tier_str, v.rule_id);
171
172 let display_name = match &v.object_kind {
173 crate::report::violations::ObjectKind::Database
174 | crate::report::violations::ObjectKind::Role
175 | crate::report::violations::ObjectKind::Publication
176 | crate::report::violations::ObjectKind::Subscription => {
177 let step1 = if let Some(idx) = v.object_name.find('.') {
178 &v.object_name[idx + 1..]
179 } else {
180 &v.object_name
181 };
182 step1
183 .strip_suffix(" (inferred)")
184 .unwrap_or(step1)
185 .to_string()
186 }
187 _ => v.object_name.clone(),
188 };
189
190 if v.object_kind == crate::report::violations::ObjectKind::Unknown {
191 println!(" object : {}", display_name);
192 } else {
193 println!(" object : {} {}", v.object_kind, display_name);
194 }
195
196 println!(" reason : {}", v.reason);
197
198 let clean_recipe = v
200 .recipe
201 .lines()
202 .map(|l| l.trim())
203 .filter(|l| !l.is_empty())
204 .collect::<Vec<_>>()
205 .join(" ");
206 println!(" recipe : {}", clean_recipe);
207
208 if let Some(sql) = &v.sql {
209 let sql_trimmed = sql.trim();
210 if !sql_trimmed.is_empty() {
211 println!(" sql : {}", sql_trimmed);
212 }
213 }
214
215 for &sec_idx in secondary_idxs {
217 let sv = &violations[sec_idx];
218 println!(
219 " also : [{}] {}",
220 tier_label_colored(&sv.tier),
221 sv.rule_id
222 );
223 }
224
225 if gi < groups.len() - 1 {
226 println!();
227 println!(" {}", "─".repeat(sep_width));
228 println!();
229 }
230 }
231
232 println!();
233
234 let mut summary_table = Table::new();
236 summary_table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
237 summary_table.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth);
238 summary_table.set_width(width as u16);
239 summary_table.set_header(vec!["SUMMARY", ""]);
240 summary_table.add_row(vec!["Verdict", &format!(": {}", verdict.label())]);
241 summary_table.add_row(vec![
242 "Recommendation",
243 &format!(": {}", verdict.recommendation()),
244 ]);
245 summary_table.add_row(vec!["HALT (Tier 1)", &format!(": {}", tier1)]);
246 summary_table.add_row(vec!["WARN (Tier 2)", &format!(": {}", tier2)]);
247 summary_table.add_row(vec!["SAFE (Tier 3)", &format!(": {}", tier3)]);
248 println!("{}", summary_table);
249
250 verdict == Verdict::Halt
251 }
252}