Skip to main content

orbok_core/
privacy.rs

1//! Privacy modes and local data visibility (RFC-039 §5–§6, §17).
2//!
3//! This is the shared vocabulary for privacy settings. It lives in
4//! `orbok-core` so that `orbok`, `orbok-ui`, and any future
5//! diagnostics layer can all refer to the same types without a
6//! circular dependency.
7
8use serde::{Deserialize, Serialize};
9
10// ── Privacy mode ──────────────────────────────────────────────────────
11
12/// Top-level privacy mode for the app (RFC-039 §5).
13///
14/// User-facing copy:
15/// - `Standard`    → "Documents are processed on this computer only."
16/// - `Strict`      → "Strict privacy reduces what orbok remembers."
17/// - `Portable`    → "orbok stores app data next to this copy of the app."
18/// - `Diagnostics` → "Include extra details for troubleshooting."
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum PrivacyMode {
22    /// Default — safe for most users.
23    #[default]
24    Standard,
25    /// Reduced local footprint for sensitive environments.
26    Strict,
27    /// Data lives next to the portable app copy.
28    Portable,
29    /// Temporary opt-in for troubleshooting (must be explicitly enabled).
30    Diagnostics,
31}
32
33impl PrivacyMode {
34    /// Stable settings string for persistence.
35    pub fn as_str(self) -> &'static str {
36        match self {
37            PrivacyMode::Standard => "standard",
38            PrivacyMode::Strict => "strict",
39            PrivacyMode::Portable => "portable",
40            PrivacyMode::Diagnostics => "diagnostics",
41        }
42    }
43
44    pub fn parse(s: &str) -> Self {
45        match s {
46            "strict" => PrivacyMode::Strict,
47            "portable" => PrivacyMode::Portable,
48            "diagnostics" => PrivacyMode::Diagnostics,
49            _ => PrivacyMode::Standard,
50        }
51    }
52
53    #[allow(clippy::should_implement_trait)]
54    pub fn from_str(s: &str) -> Self {
55        Self::parse(s)
56    }
57
58    /// Whether recent searches should be stored in this mode (RFC-039 §10).
59    pub fn allows_recent_searches(self) -> bool {
60        !matches!(self, PrivacyMode::Strict)
61    }
62
63    /// Whether snippet / preview caching is allowed (RFC-039 §11).
64    pub fn allows_snippet_persistence(self) -> bool {
65        !matches!(self, PrivacyMode::Strict)
66    }
67
68    /// Whether sensitive diagnostics opt-ins are shown (RFC-039 §14).
69    pub fn allows_diagnostics_sensitive_optins(self) -> bool {
70        !matches!(self, PrivacyMode::Strict)
71    }
72}
73
74// ── Privacy settings ──────────────────────────────────────────────────
75
76/// Fine-grained privacy preferences (RFC-039 §17).
77///
78/// `mode` governs defaults; individual fields may further restrict
79/// behavior. Strict mode forces some fields to their most private value
80/// regardless of what the user previously selected.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PrivacySettings {
83    pub mode: PrivacyMode,
84    /// Whether to persist recent search queries.
85    pub remember_recent_searches: bool,
86    /// Whether to store snippet previews across sessions.
87    pub persist_snippets: bool,
88    /// Whether to clear temporary previews when the app exits.
89    pub clear_temporary_previews_on_exit: bool,
90    /// Whether diagnostics may include raw filesystem paths.
91    pub diagnostics_include_paths: bool,
92    /// Whether diagnostics may include recent search queries.
93    pub diagnostics_include_recent_searches: bool,
94}
95
96impl Default for PrivacySettings {
97    fn default() -> Self {
98        Self {
99            mode: PrivacyMode::Standard,
100            remember_recent_searches: true,
101            persist_snippets: true,
102            clear_temporary_previews_on_exit: false,
103            diagnostics_include_paths: false,
104            diagnostics_include_recent_searches: false,
105        }
106    }
107}
108
109impl PrivacySettings {
110    /// Apply strict-mode overrides (RFC-039 §9).
111    ///
112    /// Strict mode forces the most private values for settings it
113    /// controls, regardless of individual field values.
114    pub fn with_mode_applied(mut self) -> Self {
115        if self.mode == PrivacyMode::Strict {
116            self.remember_recent_searches = false;
117            self.persist_snippets = false;
118            self.diagnostics_include_paths = false;
119            self.diagnostics_include_recent_searches = false;
120        }
121        self
122    }
123
124    /// Effective value for recent searches, accounting for mode.
125    pub fn effective_recent_searches(&self) -> bool {
126        self.mode.allows_recent_searches() && self.remember_recent_searches
127    }
128
129    /// Effective value for snippet persistence, accounting for mode.
130    pub fn effective_snippet_persistence(&self) -> bool {
131        self.mode.allows_snippet_persistence() && self.persist_snippets
132    }
133}
134
135// ── Local data category ───────────────────────────────────────────────
136
137/// Classified local data categories for the storage dashboard
138/// and cleanup controls (RFC-039 §6, §15, §16).
139///
140/// User-facing labels must avoid technical terms — see RFC-039 §15
141/// for the mapping (`KeywordIndex` → "Search data", etc.).
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum LocalDataCategory {
144    SourcePaths,
145    FileMetadata,
146    ExtractedText,
147    KeywordIndex,
148    Embeddings,
149    Snippets,
150    TemporaryPreviews,
151    RecentSearches,
152    Logs,
153    Diagnostics,
154    ModelFiles,
155    Settings,
156}
157
158impl LocalDataCategory {
159    /// Plain-language user label (RFC-039 §15 — no "cache/catalog/vector").
160    pub fn user_label(self) -> &'static str {
161        match self {
162            LocalDataCategory::SourcePaths => "Folder list",
163            LocalDataCategory::FileMetadata => "File information",
164            LocalDataCategory::ExtractedText => "Prepared text",
165            LocalDataCategory::KeywordIndex => "Search data",
166            LocalDataCategory::Embeddings => "Better search data",
167            LocalDataCategory::Snippets => "Temporary previews",
168            LocalDataCategory::TemporaryPreviews => "Temporary previews",
169            LocalDataCategory::RecentSearches => "Recent searches",
170            LocalDataCategory::Logs => "Logs",
171            LocalDataCategory::Diagnostics => "Support files",
172            LocalDataCategory::ModelFiles => "Search helper",
173            LocalDataCategory::Settings => "App settings",
174        }
175    }
176}
177
178// ── Diagnostics policy ────────────────────────────────────────────────
179
180/// Policy governing what a diagnostics export may include (RFC-040 §12).
181///
182/// All sensitive fields default to `false`. Strict privacy mode
183/// prevents enabling them.
184#[derive(Debug, Clone)]
185pub struct DiagnosticsPolicy {
186    pub include_raw_paths: bool,
187    pub include_folder_names: bool,
188    pub include_recent_searches: bool,
189    pub include_detailed_logs: bool,
190    pub privacy_mode: PrivacyMode,
191}
192
193impl Default for DiagnosticsPolicy {
194    fn default() -> Self {
195        Self {
196            include_raw_paths: false,
197            include_folder_names: false,
198            include_recent_searches: false,
199            include_detailed_logs: false,
200            privacy_mode: PrivacyMode::Standard,
201        }
202    }
203}
204
205impl DiagnosticsPolicy {
206    /// Build from privacy settings, enforcing strict-mode restrictions.
207    pub fn from_privacy(settings: &PrivacySettings) -> Self {
208        let strict = settings.mode == PrivacyMode::Strict;
209        Self {
210            include_raw_paths: false,    // never enabled by default
211            include_folder_names: false, // opt-in only
212            include_recent_searches: if strict {
213                false
214            } else {
215                settings.diagnostics_include_recent_searches
216            },
217            include_detailed_logs: false,
218            privacy_mode: settings.mode,
219        }
220    }
221
222    /// Whether this policy permits showing sensitive opt-in checkboxes.
223    pub fn allows_sensitive_optins(&self) -> bool {
224        self.privacy_mode.allows_diagnostics_sensitive_optins()
225    }
226}