Skip to main content

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}