Skip to main content

orbok_core/
status.rs

1//! Lifecycle status vocabulary shared by the catalog (RFC-002), the
2//! source boundary (RFC-003), and the scanner (RFC-004).
3//!
4//! Each enum maps 1-to-1 onto a CHECK-constrained catalog column. The
5//! `as_str`/`parse` pairs are the single conversion point; repositories
6//! must not hand-write these strings.
7
8use crate::error::OrbokError;
9use serde::{Deserialize, Serialize};
10
11macro_rules! catalog_enum {
12    ($(#[$doc:meta])* $name:ident, $column:literal, { $($variant:ident => $s:literal),+ $(,)? }) => {
13        $(#[$doc])*
14        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15        #[serde(rename_all = "snake_case")]
16        pub enum $name {
17            $($variant),+
18        }
19
20        impl $name {
21            /// Stable catalog string.
22            pub fn as_str(&self) -> &'static str {
23                match self {
24                    $(Self::$variant => $s),+
25                }
26            }
27
28            /// Parse the catalog string; invalid values are catalog
29            /// corruption and surface as a typed error.
30            pub fn parse(s: &str) -> Result<Self, OrbokError> {
31                match s {
32                    $($s => Ok(Self::$variant),)+
33                    other => Err(OrbokError::InvalidCatalogValue {
34                        column: $column,
35                        value: other.to_string(),
36                    }),
37                }
38            }
39        }
40    };
41}
42
43catalog_enum!(
44    /// `sources.source_type` (RFC-003 §5).
45    SourceType,
46    "sources.source_type",
47    { Directory => "directory", File => "file" }
48);
49
50catalog_enum!(
51    /// `sources.persistence_mode` (RFC-003 §5.1–5.2).
52    PersistenceMode,
53    "sources.persistence_mode",
54    { Persistent => "persistent", Temporary => "temporary" }
55);
56
57catalog_enum!(
58    /// `sources.status` (external design §11.2).
59    SourceStatus,
60    "sources.status",
61    {
62        Active => "active",
63        Paused => "paused",
64        Missing => "missing",
65        PermissionDenied => "permission_denied",
66        Removed => "removed",
67    }
68);
69
70impl SourceStatus {
71    /// Whether a source in this state contributes search candidates
72    /// (RFC-060 §5, §11 criterion 6).
73    ///
74    /// Mirrors `orbok_fs::source_lifecycle::SourceState::is_searchable`
75    /// for the states the catalog can actually hold: that vocabulary is
76    /// RFC-037's wider one (it adds `Preparing` and `NeedsUpdate`), while
77    /// `sources.status`'s CHECK admits only the five values above -- so a
78    /// row can never carry the two extra searchable states, and `Active`
79    /// is the whole searchable set here.
80    pub fn is_searchable(self) -> bool {
81        matches!(self, Self::Active)
82    }
83}
84
85/// Every searchable `sources.status` value as a SQL `IN` list, for the
86/// retrieval joins (RFC-060 §5). One definition rather than the literal
87/// repeated at four query sites; `searchable_status_sql_matches_the_enum`
88/// in this crate's tests keeps it equal to [`SourceStatus::is_searchable`].
89/// Built from `as_str` values, never from caller input.
90pub const SEARCHABLE_SOURCE_STATUS_SQL: &str = "('active')";
91
92catalog_enum!(
93    /// `sources.index_mode` (FR-120 quality modes).
94    IndexMode,
95    "sources.index_mode",
96    {
97        Balanced => "balanced",
98        HighAccuracy => "high_accuracy",
99        SpaceSaving => "space_saving",
100    }
101);
102
103catalog_enum!(
104    /// `sources.hidden_file_policy` (RFC-003 §6.1; default Exclude).
105    HiddenFilePolicy,
106    "sources.hidden_file_policy",
107    { Exclude => "exclude", Include => "include", Warn => "warn" }
108);
109
110catalog_enum!(
111    /// `sources.symlink_policy` (RFC-003 §6.2; default Ignore).
112    SymlinkPolicy,
113    "sources.symlink_policy",
114    {
115        Ignore => "ignore",
116        FollowWithinSource => "follow_within_source",
117        FollowAllWithWarning => "follow_all_with_warning",
118    }
119);
120
121catalog_enum!(
122    /// `files.file_status` (RFC-004 §7).
123    FileStatus,
124    "files.file_status",
125    {
126        Discovered => "discovered",
127        Indexed => "indexed",
128        Stale => "stale",
129        Missing => "missing",
130        Deleted => "deleted",
131        PermissionDenied => "permission_denied",
132        Unsupported => "unsupported",
133        Failed => "failed",
134        // Task 080: orbok read the file but its extraction produced no
135        // segments at all -- a scanned PDF is the common case. Distinct
136        // from `Indexed` (0 chunks would otherwise be silently indistinct
137        // from "prepared and searchable") and from `Discovered` (nothing
138        // is pending: the file's jobs all succeeded).
139        NoTextFound => "no_text_found",
140    }
141);
142
143catalog_enum!(
144    /// `index_jobs.job_type` (RFC-002 §7.9).
145    JobType,
146    "index_jobs.job_type",
147    {
148        Scan => "scan",
149        Extract => "extract",
150        Chunk => "chunk",
151        KeywordIndex => "keyword_index",
152        Embedding => "embedding",
153        DeleteStale => "delete_stale",
154        Rebuild => "rebuild",
155    }
156);
157
158catalog_enum!(
159    /// `index_jobs.status` (RFC-002 §7.9; RFC-036 adds Paused/WaitingForDependency).
160    JobStatus,
161    "index_jobs.status",
162    {
163        Queued => "queued",
164        Running => "running",
165        Succeeded => "succeeded",
166        Failed => "failed",
167        Canceled => "canceled",
168        Blocked => "blocked",
169        Paused => "paused",
170        WaitingForDependency => "waiting_for_dependency",
171    }
172);
173
174#[allow(clippy::derivable_impls)]
175impl Default for HiddenFilePolicy {
176    fn default() -> Self {
177        HiddenFilePolicy::Exclude
178    }
179}
180
181#[allow(clippy::derivable_impls)]
182impl Default for SymlinkPolicy {
183    fn default() -> Self {
184        SymlinkPolicy::Ignore
185    }
186}
187
188#[allow(clippy::derivable_impls)]
189impl Default for IndexMode {
190    fn default() -> Self {
191        IndexMode::Balanced
192    }
193}