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-app`, `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 from_str(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    /// Whether recent searches should be stored in this mode (RFC-039 §10).
54    pub fn allows_recent_searches(self) -> bool {
55        !matches!(self, PrivacyMode::Strict)
56    }
57
58    /// Whether snippet / preview caching is allowed (RFC-039 §11).
59    pub fn allows_snippet_persistence(self) -> bool {
60        !matches!(self, PrivacyMode::Strict)
61    }
62
63    /// Whether sensitive diagnostics opt-ins are shown (RFC-039 §14).
64    pub fn allows_diagnostics_sensitive_optins(self) -> bool {
65        !matches!(self, PrivacyMode::Strict)
66    }
67}
68
69// ── Privacy settings ──────────────────────────────────────────────────
70
71/// Fine-grained privacy preferences (RFC-039 §17).
72///
73/// `mode` governs defaults; individual fields may further restrict
74/// behavior. Strict mode forces some fields to their most private value
75/// regardless of what the user previously selected.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct PrivacySettings {
78    pub mode: PrivacyMode,
79    /// Whether to persist recent search queries.
80    pub remember_recent_searches: bool,
81    /// Whether to store snippet previews across sessions.
82    pub persist_snippets: bool,
83    /// Whether to clear temporary previews when the app exits.
84    pub clear_temporary_previews_on_exit: bool,
85    /// Whether diagnostics may include raw filesystem paths.
86    pub diagnostics_include_paths: bool,
87    /// Whether diagnostics may include recent search queries.
88    pub diagnostics_include_recent_searches: bool,
89}
90
91impl Default for PrivacySettings {
92    fn default() -> Self {
93        Self {
94            mode: PrivacyMode::Standard,
95            remember_recent_searches: true,
96            persist_snippets: true,
97            clear_temporary_previews_on_exit: false,
98            diagnostics_include_paths: false,
99            diagnostics_include_recent_searches: false,
100        }
101    }
102}
103
104impl PrivacySettings {
105    /// Apply strict-mode overrides (RFC-039 §9).
106    ///
107    /// Strict mode forces the most private values for settings it
108    /// controls, regardless of individual field values.
109    pub fn with_mode_applied(mut self) -> Self {
110        if self.mode == PrivacyMode::Strict {
111            self.remember_recent_searches = false;
112            self.persist_snippets = false;
113            self.diagnostics_include_paths = false;
114            self.diagnostics_include_recent_searches = false;
115        }
116        self
117    }
118
119    /// Effective value for recent searches, accounting for mode.
120    pub fn effective_recent_searches(&self) -> bool {
121        self.mode.allows_recent_searches() && self.remember_recent_searches
122    }
123
124    /// Effective value for snippet persistence, accounting for mode.
125    pub fn effective_snippet_persistence(&self) -> bool {
126        self.mode.allows_snippet_persistence() && self.persist_snippets
127    }
128}
129
130// ── Local data category ───────────────────────────────────────────────
131
132/// Classified local data categories for the storage dashboard
133/// and cleanup controls (RFC-039 §6, §15, §16).
134///
135/// User-facing labels must avoid technical terms — see RFC-039 §15
136/// for the mapping (`KeywordIndex` → "Search data", etc.).
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum LocalDataCategory {
139    SourcePaths,
140    FileMetadata,
141    ExtractedText,
142    KeywordIndex,
143    Embeddings,
144    Snippets,
145    TemporaryPreviews,
146    RecentSearches,
147    Logs,
148    Diagnostics,
149    ModelFiles,
150    Settings,
151}
152
153impl LocalDataCategory {
154    /// Plain-language user label (RFC-039 §15 — no "cache/catalog/vector").
155    pub fn user_label(self) -> &'static str {
156        match self {
157            LocalDataCategory::SourcePaths => "Folder list",
158            LocalDataCategory::FileMetadata => "File information",
159            LocalDataCategory::ExtractedText => "Prepared text",
160            LocalDataCategory::KeywordIndex => "Search data",
161            LocalDataCategory::Embeddings => "Better search data",
162            LocalDataCategory::Snippets => "Temporary previews",
163            LocalDataCategory::TemporaryPreviews => "Temporary previews",
164            LocalDataCategory::RecentSearches => "Recent searches",
165            LocalDataCategory::Logs => "Logs",
166            LocalDataCategory::Diagnostics => "Support files",
167            LocalDataCategory::ModelFiles => "Search helper",
168            LocalDataCategory::Settings => "App settings",
169        }
170    }
171}
172
173// ── Diagnostics policy ────────────────────────────────────────────────
174
175/// Policy governing what a diagnostics export may include (RFC-040 §12).
176///
177/// All sensitive fields default to `false`. Strict privacy mode
178/// prevents enabling them.
179#[derive(Debug, Clone)]
180pub struct DiagnosticsPolicy {
181    pub include_raw_paths: bool,
182    pub include_folder_names: bool,
183    pub include_recent_searches: bool,
184    pub include_detailed_logs: bool,
185    pub privacy_mode: PrivacyMode,
186}
187
188impl Default for DiagnosticsPolicy {
189    fn default() -> Self {
190        Self {
191            include_raw_paths: false,
192            include_folder_names: false,
193            include_recent_searches: false,
194            include_detailed_logs: false,
195            privacy_mode: PrivacyMode::Standard,
196        }
197    }
198}
199
200impl DiagnosticsPolicy {
201    /// Build from privacy settings, enforcing strict-mode restrictions.
202    pub fn from_privacy(settings: &PrivacySettings) -> Self {
203        let strict = settings.mode == PrivacyMode::Strict;
204        Self {
205            include_raw_paths: false, // never enabled by default
206            include_folder_names: if strict { false } else { false }, // opt-in only
207            include_recent_searches: if strict {
208                false
209            } else {
210                settings.diagnostics_include_recent_searches
211            },
212            include_detailed_logs: false,
213            privacy_mode: settings.mode,
214        }
215    }
216
217    /// Whether this policy permits showing sensitive opt-in checkboxes.
218    pub fn allows_sensitive_optins(&self) -> bool {
219        self.privacy_mode.allows_diagnostics_sensitive_optins()
220    }
221}