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