devclean/model.rs
1use std::fmt;
2use std::path::PathBuf;
3
4use clap::ValueEnum;
5use serde::{Deserialize, Serialize};
6
7/// A class of rebuildable development data.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, ValueEnum)]
9#[serde(rename_all = "kebab-case")]
10pub enum Category {
11 /// Cargo compilation outputs with Rust-specific markers.
12 RustTarget,
13 /// JavaScript dependency installations.
14 NodeModules,
15 /// Framework caches such as `.next` and `.svelte-kit`.
16 FrameworkCache,
17 /// Build directories underneath a recognized project manifest.
18 BuildOutput,
19 /// Test, mutation, type-checker, and lint caches.
20 TestCache,
21 /// Downloaded package-manager or tool caches.
22 GlobalCache,
23 /// Large runtimes or model caches that are expensive to restore.
24 ExpensiveGlobalCache,
25}
26
27/// Confidence assigned to a filesystem observation.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "kebab-case")]
30pub enum Confidence {
31 /// A known rebuildable artifact with filesystem-verifiable evidence.
32 #[default]
33 Safe,
34 /// A cache-like directory that must be reviewed before a cleanup rule is added.
35 Review,
36 /// A directory protected from cleanup because it may contain source or user data.
37 Protected,
38}
39
40/// A narrowly-scoped cleanup rule that Learning Mode can propose for explicit approval.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "kebab-case")]
43pub enum ReviewRule {
44 /// A `.build` directory directly beside a Swift Package manifest.
45 SwiftPackageBuild,
46}
47
48impl Category {
49 /// Returns all categories discovered by a comprehensive scan.
50 #[must_use]
51 pub fn all() -> [Self; 7] {
52 [
53 Self::RustTarget,
54 Self::NodeModules,
55 Self::FrameworkCache,
56 Self::BuildOutput,
57 Self::TestCache,
58 Self::GlobalCache,
59 Self::ExpensiveGlobalCache,
60 ]
61 }
62
63 /// Returns conservative categories used by `clean` unless overridden.
64 #[must_use]
65 pub fn safe_defaults() -> [Self; 3] {
66 [Self::RustTarget, Self::NodeModules, Self::FrameworkCache]
67 }
68}
69
70impl fmt::Display for Category {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 let value = match self {
73 Self::RustTarget => "rust-target",
74 Self::NodeModules => "node-modules",
75 Self::FrameworkCache => "framework-cache",
76 Self::BuildOutput => "build-output",
77 Self::TestCache => "test-cache",
78 Self::GlobalCache => "global-cache",
79 Self::ExpensiveGlobalCache => "expensive-global-cache",
80 };
81 formatter.write_str(value)
82 }
83}
84
85/// A directory that can be rebuilt or downloaded again.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Candidate {
88 /// Artifact category.
89 pub category: Category,
90 /// Absolute or user-supplied path to the artifact.
91 pub path: PathBuf,
92 /// Estimated allocated bytes on disk.
93 pub bytes: u64,
94 /// Evidence used to classify the directory.
95 pub reason: String,
96 /// Latest observed modification time as seconds since the Unix epoch.
97 pub modified_at_unix: Option<u64>,
98 /// Safety confidence assigned by the scanner.
99 #[serde(default)]
100 pub confidence: Confidence,
101 /// Explicit learned rule that authorized this candidate, if any.
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub approved_rule: Option<ReviewRule>,
104}
105
106/// A large cache-like directory observed by Learning Mode but never selected for cleanup.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ReviewCandidate {
109 /// Absolute path retained only in the local report.
110 pub path: PathBuf,
111 /// Estimated allocated bytes on disk.
112 pub bytes: u64,
113 /// Evidence that made the directory interesting to Learning Mode.
114 pub reason: String,
115 /// Latest observed modification time as seconds since the Unix epoch.
116 pub modified_at_unix: Option<u64>,
117 /// Review candidates are never promoted to safe without an explicit product rule.
118 pub confidence: Confidence,
119 /// Product rule that can safely constrain an approval for this path.
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub suggested_rule: Option<ReviewRule>,
122 /// Project root to which the suggested rule would be scoped.
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub project_root: Option<PathBuf>,
125 /// Whether the user has approved the suggested rule for this exact path.
126 #[serde(default)]
127 pub approved: bool,
128}
129
130/// One local-only Learning Mode measurement independent of cleanup eligibility filters.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct LearningObservation {
133 /// Observed artifact path. Remote telemetry must never include this value.
134 pub path: PathBuf,
135 /// Known category when the scanner can classify the artifact safely.
136 pub category: Option<Category>,
137 /// Estimated allocated bytes on disk.
138 pub bytes: u64,
139 /// Filesystem evidence behind the observation.
140 pub reason: String,
141 /// Latest observed modification time as seconds since the Unix epoch.
142 pub modified_at_unix: Option<u64>,
143 /// Safety confidence independent of cleanup age and size filters.
144 pub confidence: Confidence,
145}
146
147/// Result of scanning one or more roots.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ScanReport {
150 /// Roots traversed by the scanner.
151 pub roots: Vec<PathBuf>,
152 /// Rebuildable directories found.
153 pub candidates: Vec<Candidate>,
154 /// Cache-like directories that require review and cannot be cleaned yet.
155 #[serde(default)]
156 pub review_candidates: Vec<ReviewCandidate>,
157 /// Local measurements used for growth history, including active artifacts filtered from cleanup.
158 #[serde(default)]
159 pub learning_observations: Vec<LearningObservation>,
160 /// Non-fatal traversal or metadata errors.
161 pub warnings: Vec<String>,
162 /// Total estimated allocated bytes for all candidates.
163 pub total_bytes: u64,
164 /// Total allocated bytes represented by review-only observations.
165 #[serde(default)]
166 pub review_total_bytes: u64,
167 /// Total allocated bytes represented by Learning Mode measurements.
168 #[serde(default)]
169 pub observed_total_bytes: u64,
170 /// Whether cleanup must repeat the Git tracked-file guard.
171 #[serde(default = "default_true")]
172 pub protect_git_tracked: bool,
173}
174
175const fn default_true() -> bool {
176 true
177}
178
179/// Human- or machine-readable report format.
180#[derive(Debug, Clone, Copy, ValueEnum)]
181pub enum OutputFormat {
182 /// Compact terminal table.
183 Table,
184 /// Structured JSON.
185 Json,
186 /// One JSON object per line for streaming automation.
187 Jsonl,
188 /// Standalone HTML document.
189 Html,
190}
191
192/// Controls presentation without changing the underlying scan result.
193#[derive(Debug, Clone, Copy, Default)]
194pub struct RenderOptions {
195 /// Replace absolute paths with stable root-relative placeholders.
196 pub redact_paths: bool,
197}