1use std::cmp::Reverse;
5use std::path::{Path, PathBuf};
6
7use anyhow::Result;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct ScanSummarySnapshot {
14 pub files_analyzed: u64,
15 pub files_skipped: u64,
16 pub total_physical_lines: u64,
17 pub code_lines: u64,
18 pub comment_lines: u64,
19 pub blank_lines: u64,
20 #[serde(default)]
21 pub functions: u64,
22 #[serde(default)]
23 pub classes: u64,
24 #[serde(default)]
25 pub variables: u64,
26 #[serde(default)]
27 pub imports: u64,
28 #[serde(default)]
29 pub test_count: u64,
30 #[serde(default)]
31 pub coverage_lines_found: u64,
32 #[serde(default)]
33 pub coverage_lines_hit: u64,
34 #[serde(default)]
35 pub coverage_functions_found: u64,
36 #[serde(default)]
37 pub coverage_functions_hit: u64,
38 #[serde(default)]
39 pub coverage_branches_found: u64,
40 #[serde(default)]
41 pub coverage_branches_hit: u64,
42}
43
44impl From<&crate::SummaryTotals> for ScanSummarySnapshot {
45 fn from(t: &crate::SummaryTotals) -> Self {
49 Self {
50 files_analyzed: t.files_analyzed,
51 files_skipped: t.files_skipped,
52 total_physical_lines: t.total_physical_lines,
53 code_lines: t.code_lines,
54 comment_lines: t.comment_lines,
55 blank_lines: t.blank_lines,
56 functions: t.functions,
57 classes: t.classes,
58 variables: t.variables,
59 imports: t.imports,
60 test_count: t.test_count,
61 coverage_lines_found: t.coverage_lines_found,
62 coverage_lines_hit: t.coverage_lines_hit,
63 coverage_functions_found: t.coverage_functions_found,
64 coverage_functions_hit: t.coverage_functions_hit,
65 coverage_branches_found: t.coverage_branches_found,
66 coverage_branches_hit: t.coverage_branches_hit,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RegistryEntry {
74 pub run_id: String,
75 pub timestamp_utc: DateTime<Utc>,
76 pub project_label: String,
77 pub input_roots: Vec<String>,
78 pub json_path: Option<PathBuf>,
79 pub html_path: Option<PathBuf>,
80 #[serde(default)]
81 pub pdf_path: Option<PathBuf>,
82 #[serde(default)]
83 pub csv_path: Option<PathBuf>,
84 #[serde(default)]
85 pub xlsx_path: Option<PathBuf>,
86 pub summary: ScanSummarySnapshot,
87 #[serde(default)]
89 pub git_branch: Option<String>,
90 #[serde(default)]
92 pub git_commit: Option<String>,
93 #[serde(default)]
95 pub git_author: Option<String>,
96 #[serde(default)]
98 pub git_tags: Option<String>,
99 #[serde(default)]
101 pub git_nearest_tag: Option<String>,
102 #[serde(default)]
104 pub git_commit_date: Option<String>,
105}
106
107#[derive(Debug, Default, Serialize, Deserialize)]
110pub struct WatchedDirsStore {
111 pub dirs: Vec<PathBuf>,
112}
113
114impl WatchedDirsStore {
115 #[must_use]
116 pub fn load(path: &Path) -> Self {
117 std::fs::read_to_string(path)
118 .ok()
119 .and_then(|s| serde_json::from_str(&s).ok())
120 .unwrap_or_default()
121 }
122
123 pub fn save(&self, path: &Path) -> Result<()> {
127 if let Some(parent) = path.parent() {
128 std::fs::create_dir_all(parent)?;
129 }
130 std::fs::write(path, serde_json::to_string_pretty(self)?)?;
131 Ok(())
132 }
133
134 pub fn add(&mut self, dir: PathBuf) {
135 if !self.dirs.contains(&dir) {
136 self.dirs.push(dir);
137 }
138 }
139
140 pub fn remove(&mut self, dir: &Path) {
141 self.dirs.retain(|d| d != dir);
142 }
143}
144
145#[derive(Debug, Default, Serialize, Deserialize)]
148pub struct ScanRegistry {
149 pub entries: Vec<RegistryEntry>,
150}
151
152impl ScanRegistry {
153 #[must_use]
155 pub fn load(registry_path: &Path) -> Self {
156 std::fs::read_to_string(registry_path)
157 .ok()
158 .and_then(|s| serde_json::from_str(&s).ok())
159 .unwrap_or_default()
160 }
161
162 pub fn save(&self, registry_path: &Path) -> Result<()> {
166 if let Some(parent) = registry_path.parent() {
167 std::fs::create_dir_all(parent)?;
168 }
169 let json = serde_json::to_string_pretty(self)?;
170 std::fs::write(registry_path, json)?;
171 Ok(())
172 }
173
174 pub fn add_entry(&mut self, entry: RegistryEntry) {
175 self.entries.retain(|e| e.run_id != entry.run_id);
176 self.entries.push(entry);
177 self.entries.sort_by_key(|e| Reverse(e.timestamp_utc));
178 }
179
180 #[must_use]
182 pub fn entries_for_roots(&self, roots: &[String]) -> Vec<&RegistryEntry> {
183 self.entries
184 .iter()
185 .filter(|e| e.input_roots == roots)
186 .collect()
187 }
188
189 #[must_use]
190 pub fn find_by_run_id(&self, run_id: &str) -> Option<&RegistryEntry> {
191 self.entries.iter().find(|e| e.run_id == run_id)
192 }
193
194 pub fn prune_stale(&mut self) {
196 self.entries
197 .retain(|e| e.json_path.as_ref().is_none_or(|p| p.exists()));
198 }
199}
200
201const fn default_interval_hours() -> u32 {
202 24
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct CleanupPolicy {
208 pub enabled: bool,
209 #[serde(default)]
211 pub max_age_days: Option<u32>,
212 #[serde(default)]
214 pub max_run_count: Option<u32>,
215 #[serde(default = "default_interval_hours")]
217 pub interval_hours: u32,
218}
219
220#[derive(Debug, Default, Clone, Serialize, Deserialize)]
223pub struct CleanupPolicyStore {
224 pub policy: Option<CleanupPolicy>,
225 #[serde(default)]
227 pub last_run_at: Option<DateTime<Utc>>,
228 #[serde(default)]
230 pub last_run_deleted: Option<u32>,
231}
232
233impl CleanupPolicyStore {
234 #[must_use]
235 pub fn load(path: &Path) -> Self {
236 std::fs::read_to_string(path)
237 .ok()
238 .and_then(|s| serde_json::from_str(&s).ok())
239 .unwrap_or_default()
240 }
241
242 pub fn save(&self, path: &Path) -> Result<()> {
246 if let Some(parent) = path.parent() {
247 std::fs::create_dir_all(parent)?;
248 }
249 std::fs::write(path, serde_json::to_string_pretty(self)?)?;
250 Ok(())
251 }
252}