Skip to main content

orbok_core/
data_class.rs

1//! RFC-001 data lifecycle classification.
2//!
3//! Every piece of application-managed data belongs to exactly one
4//! [`DataClass`]. Cleanup operations must be expressed as a
5//! [`CleanupPlan`] before execution (RFC-001 §14: "No cleanup operation
6//! should run without first producing a `CleanupPlan`"). Ordinary (safe)
7//! cleanup must never touch [`DataClass::PersistentCatalog`].
8
9use serde::{Deserialize, Serialize};
10
11/// The five lifecycle classes of RFC-001 §14.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum DataClass {
15    /// User configuration and known catalog state. Never deleted by
16    /// ordinary cleanup (sources, policies, file catalog, settings,
17    /// model registry, migrations).
18    PersistentCatalog,
19    /// Derived from source files and local models; deletable with
20    /// confirmation, rebuildable (keyword index, embeddings, chunks).
21    RebuildableIndex,
22    /// Speed/convenience only; deletable automatically by TTL/LRU
23    /// (search cache, snippets, rerank scores, extraction buffers).
24    EphemeralCache,
25    /// Local model files: removable only with strong confirmation.
26    LocalDependency,
27    /// Logs and events, deletable under log policy.
28    OperationalLog,
29}
30
31/// Storage accounting categories (RFC-001 §10, RFC-002 §7.12).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum StorageCategory {
35    PersistentCatalog,
36    KeywordIndex,
37    VectorIndex,
38    SnippetCache,
39    SearchCache,
40    TemporaryExtraction,
41    ModelFiles,
42    Logs,
43}
44
45impl StorageCategory {
46    /// All categories, for iteration in accounting and the Storage view.
47    pub const ALL: [StorageCategory; 8] = [
48        StorageCategory::PersistentCatalog,
49        StorageCategory::KeywordIndex,
50        StorageCategory::VectorIndex,
51        StorageCategory::SnippetCache,
52        StorageCategory::SearchCache,
53        StorageCategory::TemporaryExtraction,
54        StorageCategory::ModelFiles,
55        StorageCategory::Logs,
56    ];
57
58    /// Catalog/key string (matches RFC-001 §10 names).
59    pub fn as_str(&self) -> &'static str {
60        match self {
61            StorageCategory::PersistentCatalog => "persistent_catalog",
62            StorageCategory::KeywordIndex => "keyword_index",
63            StorageCategory::VectorIndex => "vector_index",
64            StorageCategory::SnippetCache => "snippet_cache",
65            StorageCategory::SearchCache => "search_cache",
66            StorageCategory::TemporaryExtraction => "temporary_extraction",
67            StorageCategory::ModelFiles => "model_files",
68            StorageCategory::Logs => "logs",
69        }
70    }
71
72    /// The lifecycle class this storage category belongs to.
73    pub fn data_class(&self) -> DataClass {
74        match self {
75            StorageCategory::PersistentCatalog => DataClass::PersistentCatalog,
76            StorageCategory::KeywordIndex | StorageCategory::VectorIndex => {
77                DataClass::RebuildableIndex
78            }
79            StorageCategory::SnippetCache
80            | StorageCategory::SearchCache
81            | StorageCategory::TemporaryExtraction => DataClass::EphemeralCache,
82            StorageCategory::ModelFiles => DataClass::LocalDependency,
83            StorageCategory::Logs => DataClass::OperationalLog,
84        }
85    }
86}
87
88/// One category's measured storage (RFC-011 §11, Task 081). Never a zero
89/// standing in for "not measured": a category orbok cannot measure exactly
90/// is [`Self::Unknown`], rendered as such, and left out of any total --
91/// the false-zero the Storage page showed before this measured every
92/// namespace's `usage()` had no caller (Review Request 257 §3).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum StorageMeasurement {
95    Measured { bytes: u64, items: u64 },
96    Unknown,
97}
98
99/// Cleanup actions exposed by the Storage view (RFC-001 §9, RFC-011).
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum CleanupAction {
103    /// Safe: expired search cache entries.
104    ClearExpiredSearchCache,
105    /// Safe: expired/temporary snippets.
106    ClearSnippetCache,
107    /// Safe: temporary extraction buffers and cache-engine payloads.
108    ClearTemporaryExtraction,
109    /// Safe: stale indexes that have already been replaced.
110    RemoveReplacedStaleIndexes,
111    /// Space recovery: delete keyword index (rebuild required).
112    DeleteKeywordIndex,
113    /// Space recovery: delete vector index / embeddings (rebuild required).
114    DeleteVectorIndex,
115    /// Space recovery: delete temporary-source indexes.
116    RemoveTemporarySourceIndexes,
117    /// Destructive: reset the whole catalog (strong confirmation).
118    ResetCatalog,
119}
120
121impl CleanupAction {
122    /// Lifecycle classes this action is allowed to touch.
123    pub fn affected_classes(&self) -> &'static [DataClass] {
124        match self {
125            CleanupAction::ClearExpiredSearchCache
126            | CleanupAction::ClearSnippetCache
127            | CleanupAction::ClearTemporaryExtraction => &[DataClass::EphemeralCache],
128            CleanupAction::RemoveReplacedStaleIndexes
129            | CleanupAction::DeleteKeywordIndex
130            | CleanupAction::DeleteVectorIndex
131            | CleanupAction::RemoveTemporarySourceIndexes => &[DataClass::RebuildableIndex],
132            CleanupAction::ResetCatalog => &[
133                DataClass::PersistentCatalog,
134                DataClass::RebuildableIndex,
135                DataClass::EphemeralCache,
136                DataClass::OperationalLog,
137            ],
138        }
139    }
140
141    /// Whether running this action makes reindexing necessary.
142    pub fn requires_rebuild(&self) -> bool {
143        !matches!(
144            self,
145            CleanupAction::ClearExpiredSearchCache
146                | CleanupAction::ClearSnippetCache
147                | CleanupAction::ClearTemporaryExtraction
148        )
149    }
150
151    /// Whether the UI must show an explicit confirmation dialog.
152    pub fn requires_confirmation(&self) -> bool {
153        self.requires_rebuild()
154    }
155
156    /// Whether this action may touch persistent catalog data.
157    pub fn touches_persistent_catalog(&self) -> bool {
158        self.affected_classes()
159            .contains(&DataClass::PersistentCatalog)
160    }
161}
162
163/// A cleanup plan: produced first, executed second (RFC-001 §14).
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct CleanupPlan {
166    pub action: CleanupAction,
167    pub affected_classes: Vec<DataClass>,
168    pub estimated_recovered_bytes: u64,
169    pub requires_rebuild: bool,
170    pub requires_confirmation: bool,
171}
172
173impl CleanupPlan {
174    /// Build the plan for an action with an estimated byte impact.
175    pub fn for_action(action: CleanupAction, estimated_recovered_bytes: u64) -> Self {
176        Self {
177            action,
178            affected_classes: action.affected_classes().to_vec(),
179            estimated_recovered_bytes,
180            requires_rebuild: action.requires_rebuild(),
181            requires_confirmation: action.requires_confirmation(),
182        }
183    }
184
185    /// Safe cleanup must never include the persistent catalog. Executors
186    /// call this before running anything that has not been explicitly
187    /// confirmed as a destructive reset.
188    pub fn assert_safe_for_ordinary_cleanup(&self) -> Result<(), crate::error::OrbokError> {
189        if self
190            .affected_classes
191            .contains(&DataClass::PersistentCatalog)
192        {
193            return Err(crate::error::OrbokError::CleanupWouldTouchPersistentData);
194        }
195        Ok(())
196    }
197}