1use std::collections::HashMap;
8use std::fmt;
9
10use serde::Serialize;
11
12use crate::audit::{AuditEntry, AuditReport, CoverageSummary, SpecItemKind, TracePart};
13
14#[derive(Debug, Clone, Serialize)]
16pub struct DiffReport {
17 pub module_name: String,
18 pub changes: Vec<DiffEntry>,
19 pub summary: DiffSummary,
20}
21
22#[derive(Debug, Clone, Serialize)]
24pub struct DiffEntry {
25 pub kind: SpecItemKind,
26 pub name: String,
27 pub change: ChangeKind,
28 pub details: Vec<DiffDetail>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
34pub enum ChangeKind {
35 Added,
36 Removed,
37 Modified,
38}
39
40#[derive(Debug, Clone, Serialize)]
42pub struct DiffDetail {
43 pub label: String,
44 pub change: ChangeKind,
45 pub old_desc: Option<String>,
46 pub new_desc: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize)]
51pub struct DiffSummary {
52 pub added: usize,
53 pub removed: usize,
54 pub modified: usize,
55 pub old_coverage: CoverageSummary,
56 pub new_coverage: CoverageSummary,
57 pub old_verify_errors: usize,
58 pub new_verify_errors: usize,
59 pub old_obligations: usize,
60 pub new_obligations: usize,
61}
62
63pub fn diff_reports(old: &AuditReport, new: &AuditReport) -> DiffReport {
65 let old_map: HashMap<(SpecItemKind, &str), &AuditEntry> = old
67 .entries
68 .iter()
69 .map(|e| ((e.kind, e.name.as_str()), e))
70 .collect();
71
72 let new_map: HashMap<(SpecItemKind, &str), &AuditEntry> = new
73 .entries
74 .iter()
75 .map(|e| ((e.kind, e.name.as_str()), e))
76 .collect();
77
78 let mut changes = Vec::new();
79
80 for entry in &old.entries {
82 let key = (entry.kind, entry.name.as_str());
83 if !new_map.contains_key(&key) {
84 changes.push(DiffEntry {
85 kind: entry.kind,
86 name: entry.name.clone(),
87 change: ChangeKind::Removed,
88 details: vec![],
89 });
90 }
91 }
92
93 for entry in &new.entries {
95 let key = (entry.kind, entry.name.as_str());
96 match old_map.get(&key) {
97 None => {
98 changes.push(DiffEntry {
99 kind: entry.kind,
100 name: entry.name.clone(),
101 change: ChangeKind::Added,
102 details: vec![],
103 });
104 }
105 Some(old_entry) => {
106 let details = diff_parts(&old_entry.parts, &entry.parts);
107 if !details.is_empty() {
108 changes.push(DiffEntry {
109 kind: entry.kind,
110 name: entry.name.clone(),
111 change: ChangeKind::Modified,
112 details,
113 });
114 }
115 }
116 }
117 }
118
119 let added = changes
120 .iter()
121 .filter(|c| c.change == ChangeKind::Added)
122 .count();
123 let removed = changes
124 .iter()
125 .filter(|c| c.change == ChangeKind::Removed)
126 .count();
127 let modified = changes
128 .iter()
129 .filter(|c| c.change == ChangeKind::Modified)
130 .count();
131
132 let summary = DiffSummary {
133 added,
134 removed,
135 modified,
136 old_coverage: old.summary.clone(),
137 new_coverage: new.summary.clone(),
138 old_verify_errors: old.verify_error_count,
139 new_verify_errors: new.verify_error_count,
140 old_obligations: old.obligations.len(),
141 new_obligations: new.obligations.len(),
142 };
143
144 DiffReport {
145 module_name: new.module_name.clone(),
146 changes,
147 summary,
148 }
149}
150
151fn diff_parts(old_parts: &[TracePart], new_parts: &[TracePart]) -> Vec<DiffDetail> {
153 let old_map: HashMap<&str, &TracePart> =
154 old_parts.iter().map(|p| (p.label.as_str(), p)).collect();
155 let new_map: HashMap<&str, &TracePart> =
156 new_parts.iter().map(|p| (p.label.as_str(), p)).collect();
157
158 let mut details = Vec::new();
159
160 for part in old_parts {
162 if !new_map.contains_key(part.label.as_str()) {
163 details.push(DiffDetail {
164 label: part.label.clone(),
165 change: ChangeKind::Removed,
166 old_desc: Some(part.ir_desc.clone()),
167 new_desc: None,
168 });
169 }
170 }
171
172 for part in new_parts {
174 match old_map.get(part.label.as_str()) {
175 None => {
176 details.push(DiffDetail {
177 label: part.label.clone(),
178 change: ChangeKind::Added,
179 old_desc: None,
180 new_desc: Some(part.ir_desc.clone()),
181 });
182 }
183 Some(old_part) => {
184 if old_part.ir_desc != part.ir_desc {
185 details.push(DiffDetail {
186 label: part.label.clone(),
187 change: ChangeKind::Modified,
188 old_desc: Some(old_part.ir_desc.clone()),
189 new_desc: Some(part.ir_desc.clone()),
190 });
191 }
192 }
193 }
194 }
195
196 details
197}
198
199impl fmt::Display for ChangeKind {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 match self {
204 ChangeKind::Added => write!(f, "+"),
205 ChangeKind::Removed => write!(f, "-"),
206 ChangeKind::Modified => write!(f, "~"),
207 }
208 }
209}
210
211impl DiffReport {
212 pub fn format(&self) -> String {
214 let mut out = format!("{} — Spec Diff Report\n", self.module_name);
215
216 if self.changes.is_empty() {
217 out.push_str("\n No spec-level changes detected.\n");
218 return out;
219 }
220
221 for entry in &self.changes {
222 out.push('\n');
223 let symbol = entry.change;
224 out.push_str(&format!(" [{symbol}] {} {}\n", entry.kind, entry.name,));
225
226 for detail in &entry.details {
227 let dsym = detail.change;
228 match detail.change {
229 ChangeKind::Added => {
230 out.push_str(&format!(
231 " [{dsym}] {}: {}\n",
232 detail.label,
233 detail.new_desc.as_deref().unwrap_or(""),
234 ));
235 }
236 ChangeKind::Removed => {
237 out.push_str(&format!(
238 " [{dsym}] {}: {}\n",
239 detail.label,
240 detail.old_desc.as_deref().unwrap_or(""),
241 ));
242 }
243 ChangeKind::Modified => {
244 out.push_str(&format!(
245 " [{dsym}] {}: {} -> {}\n",
246 detail.label,
247 detail.old_desc.as_deref().unwrap_or(""),
248 detail.new_desc.as_deref().unwrap_or(""),
249 ));
250 }
251 }
252 }
253 }
254
255 let s = &self.summary;
257 out.push_str(&format!(
258 "\nSummary: {} added, {} removed, {} modified\n",
259 s.added, s.removed, s.modified,
260 ));
261
262 let old_total = s.old_coverage.total();
264 let new_total = s.new_coverage.total();
265 if new_total != old_total {
266 let delta = new_total as isize - old_total as isize;
267 let sign = if delta > 0 { "+" } else { "" };
268 out.push_str(&format!(
269 "Coverage: {} -> {} spec items ({sign}{})\n",
270 old_total, new_total, delta,
271 ));
272 } else {
273 out.push_str(&format!("Coverage: {} spec items (unchanged)\n", new_total));
274 }
275
276 match (s.old_verify_errors, s.new_verify_errors) {
278 (0, 0) => out.push_str("Verification: clean -> clean\n"),
279 (0, n) => out.push_str(&format!("Verification: clean -> {} error(s)\n", n)),
280 (o, 0) => out.push_str(&format!("Verification: {} error(s) -> clean\n", o)),
281 (o, n) if o == n => {
282 out.push_str(&format!("Verification: {} error(s) (unchanged)\n", n))
283 }
284 (o, n) => out.push_str(&format!("Verification: {} -> {} error(s)\n", o, n)),
285 }
286
287 if s.new_obligations != s.old_obligations {
289 out.push_str(&format!(
290 "Obligations: {} -> {}\n",
291 s.old_obligations, s.new_obligations,
292 ));
293 }
294
295 out
296 }
297}