Skip to main content

ic_query/cache/
model.rs

1//! Module: cache::model
2//!
3//! Responsibility: define shared cache-validation and local inventory report contracts.
4//! Does not own: filesystem traversal, cache-family validation, or CLI output.
5//! Boundary: names family validation outcomes and exposes generic header, age,
6//! recovery-policy, and lock evidence without performing family-specific validation.
7
8use serde::{Deserialize as SerdeDeserialize, Serialize};
9use std::{fmt, path::PathBuf};
10
11/// Current serialized schema version for cache-status reports.
12pub const CACHE_STATUS_REPORT_SCHEMA_VERSION: u32 = 1;
13
14///
15/// CacheValidationStatus
16///
17/// Semantic validation result for an existing family-specific cache.
18///
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
21#[serde(rename_all = "snake_case")]
22pub enum CacheValidationStatus {
23    /// The cache passed its family-specific schema, identity, and completeness checks.
24    #[serde(rename = "ok")]
25    Valid,
26    /// The cache exists but failed family-specific validation.
27    Invalid,
28}
29
30impl CacheValidationStatus {
31    /// Return the stable serialized status label.
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::Valid => "ok",
36            Self::Invalid => "invalid",
37        }
38    }
39}
40
41impl fmt::Display for CacheValidationStatus {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter.write_str(self.as_str())
44    }
45}
46
47///
48/// CacheRefreshAttemptStatus
49///
50/// Lifecycle state for a complete-cache refresh attempt.
51///
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CacheRefreshAttemptStatus {
56    /// A refresh started or published intermediate collection progress.
57    Running,
58    /// A refresh exhausted its source and published a complete cache.
59    Complete,
60    /// A refresh terminated without replacing the complete cache.
61    Failed,
62}
63
64impl CacheRefreshAttemptStatus {
65    /// Return the stable serialized lifecycle label.
66    #[must_use]
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Self::Running => "running",
70            Self::Complete => "complete",
71            Self::Failed => "failed",
72        }
73    }
74}
75
76impl fmt::Display for CacheRefreshAttemptStatus {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        formatter.write_str(self.as_str())
79    }
80}
81
82///
83/// CacheHeaderStatus
84///
85/// Generic header-integrity classification for one complete cache file.
86///
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
89#[serde(rename_all = "snake_case")]
90pub enum CacheHeaderStatus {
91    /// The generic cache header is readable.
92    Readable,
93    /// The generic cache header cannot be read or parsed.
94    Invalid,
95}
96
97impl CacheHeaderStatus {
98    /// Return the stable serialized status label.
99    #[must_use]
100    pub const fn as_str(self) -> &'static str {
101        match self {
102            Self::Readable => "readable",
103            Self::Invalid => "invalid",
104        }
105    }
106}
107
108///
109/// CacheAgeStatus
110///
111/// Caller-relative age classification for one complete cache file.
112///
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
115#[serde(rename_all = "snake_case")]
116pub enum CacheAgeStatus {
117    /// The cache is within its registered family stale threshold.
118    Fresh,
119    /// The cache is older than its registered family stale threshold.
120    Stale,
121    /// The cache has a readable age but no registered family stale threshold.
122    Unmanaged,
123    /// The cache age cannot be calculated from its generic timestamp evidence.
124    Unknown,
125}
126
127impl CacheAgeStatus {
128    /// Return the stable serialized status label.
129    #[must_use]
130    pub const fn as_str(self) -> &'static str {
131        match self {
132            Self::Fresh => "fresh",
133            Self::Stale => "stale",
134            Self::Unmanaged => "unmanaged",
135            Self::Unknown => "unknown",
136        }
137    }
138}
139
140///
141/// CacheRecoveryPolicy
142///
143/// Owner policy for replacing recoverable invalid content at a canonical cache path.
144///
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
147#[serde(rename_all = "snake_case")]
148pub enum CacheRecoveryPolicy {
149    /// An ordinary owner read-through replaces recoverable invalid content.
150    Automatic,
151    /// Recovery requires an explicitly selected refresh operation.
152    Explicit,
153    /// Ordinary read-through creates a missing cache but does not replace invalid content.
154    MissingOnly,
155    /// The path does not identify a current canonical cache owner.
156    Unknown,
157}
158
159impl CacheRecoveryPolicy {
160    /// Return the stable serialized policy label.
161    #[must_use]
162    pub const fn as_str(self) -> &'static str {
163        match self {
164            Self::Automatic => "automatic",
165            Self::Explicit => "explicit",
166            Self::MissingOnly => "missing_only",
167            Self::Unknown => "unknown",
168        }
169    }
170}
171
172///
173/// CacheRefreshLockStatus
174///
175/// Observed kernel exclusion or invalid coordination-file classification.
176///
177
178#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
179#[serde(rename_all = "snake_case")]
180pub enum CacheRefreshLockStatus {
181    /// Another open descriptor holds exclusive refresh ownership.
182    Active,
183    /// The persistent coordination file is currently unlocked.
184    Available,
185    /// The coordination file is invalid.
186    Invalid,
187}
188
189impl CacheRefreshLockStatus {
190    /// Return the stable serialized status label.
191    #[must_use]
192    pub const fn as_str(self) -> &'static str {
193        match self {
194            Self::Active => "active",
195            Self::Available => "available",
196            Self::Invalid => "invalid",
197        }
198    }
199}
200
201///
202/// CacheStatusRequest
203///
204/// Local cache-root inspection request with a caller-supplied observation time.
205///
206
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct CacheStatusRequest {
209    /// User-level cache root to inspect.
210    pub cache_root: PathBuf,
211    /// Observation time used to calculate cache ages.
212    pub now_unix_secs: u64,
213}
214
215impl CacheStatusRequest {
216    /// Construct a local cache-status request.
217    #[must_use]
218    pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
219        Self {
220            cache_root: cache_root.into(),
221            now_unix_secs,
222        }
223    }
224}
225
226///
227/// CacheStatusReport
228///
229/// Bounded local inventory of known complete caches and refresh locks.
230///
231
232#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
233pub struct CacheStatusReport {
234    /// Cache-status report schema version.
235    pub schema_version: u32,
236    /// Inspected user-level cache root.
237    pub cache_root: String,
238    /// UTC timestamp at which local inspection was requested.
239    pub inspected_at: String,
240    /// Whether the cache root existed.
241    pub cache_root_found: bool,
242    /// Maximum number of cache and refresh-lock files inspected in one report.
243    pub scan_limit: usize,
244    /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
245    pub truncated: bool,
246    /// Whether the generic inventory performed family-specific semantic validation.
247    pub family_validation_performed: bool,
248    /// Number of caches with readable generic headers.
249    pub readable_header_count: usize,
250    /// Number of caches with unreadable or malformed generic headers.
251    pub invalid_header_count: usize,
252    /// Number of caches fresh under an explicit family policy.
253    pub fresh_count: usize,
254    /// Number of caches stale under an explicit family policy.
255    pub stale_count: usize,
256    /// Number of readable caches whose family has no registered age policy.
257    pub unmanaged_age_count: usize,
258    /// Number of caches whose age cannot be calculated from generic evidence.
259    pub unknown_age_count: usize,
260    /// Sum of filesystem sizes for returned cache files.
261    pub total_size_bytes: u64,
262    /// Canonically path-ordered cache rows.
263    pub caches: Vec<CacheStatusRow>,
264    /// Number of coordination files held by a refreshing process.
265    pub active_refresh_lock_count: usize,
266    /// Number of currently unlocked coordination files.
267    pub available_refresh_lock_count: usize,
268    /// Number of invalid coordination files.
269    pub invalid_refresh_lock_count: usize,
270    /// Sum of filesystem sizes for returned refresh-lock files.
271    pub refresh_lock_size_bytes: u64,
272    /// Canonically path-ordered refresh-lock rows.
273    pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
274}
275
276///
277/// CacheStatusRow
278///
279/// Generic local metadata and caller-relative age for one complete cache file.
280///
281
282#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
283pub struct CacheStatusRow {
284    /// Stable component label inferred from cache identity or canonical path.
285    pub component: String,
286    /// Absolute cache-file path.
287    pub cache_path: String,
288    /// Cache-root-relative path.
289    pub relative_path: String,
290    /// Generic cache-header integrity without family-specific semantic validation.
291    pub header_status: CacheHeaderStatus,
292    /// Caller-relative age classification kept separate from header integrity.
293    pub age_status: CacheAgeStatus,
294    /// Owner policy for recovering invalid content at this canonical path.
295    pub recovery_policy: CacheRecoveryPolicy,
296    /// Serialized cache schema version when readable.
297    pub schema_version: Option<u32>,
298    /// Serialized network identity when present.
299    pub network: Option<String>,
300    /// Cache collection timestamp when readable.
301    pub fetched_at: Option<String>,
302    /// Caller-relative age when the timestamp is valid and not in the future.
303    pub age_seconds: Option<u64>,
304    /// Family age threshold when one is explicitly defined.
305    pub stale_after_seconds: Option<u64>,
306    /// Filesystem size of this cache file.
307    pub size_bytes: u64,
308    /// Generic header or timestamp inspection error; family-specific validation is separate.
309    pub inspection_error: Option<String>,
310}
311
312///
313/// CacheRefreshLockStatusRow
314///
315/// Local path and observed kernel exclusion for one persistent coordination file.
316///
317
318#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
319pub struct CacheRefreshLockStatusRow {
320    /// Stable component label inferred from the coordination-file path.
321    pub component: String,
322    /// Absolute refresh-lock path.
323    pub refresh_lock_path: String,
324    /// Cache-root-relative refresh-lock path.
325    pub relative_path: String,
326    /// Observed kernel exclusion or invalid sidecar classification.
327    pub status: CacheRefreshLockStatus,
328    /// Filesystem size of this refresh-lock file.
329    pub size_bytes: u64,
330    /// Coordination-file admission failure.
331    pub error: Option<String>,
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn typed_statuses_keep_stable_json_labels() {
340        for (status, expected) in [
341            (CacheValidationStatus::Valid, "ok"),
342            (CacheValidationStatus::Invalid, "invalid"),
343        ] {
344            assert_eq!(status.as_str(), expected);
345            assert_eq!(status.to_string(), expected);
346            assert_eq!(
347                serde_json::to_value(status).expect("serialize cache validation status"),
348                serde_json::json!(expected)
349            );
350        }
351        for (status, expected) in [
352            (CacheRefreshAttemptStatus::Running, "running"),
353            (CacheRefreshAttemptStatus::Complete, "complete"),
354            (CacheRefreshAttemptStatus::Failed, "failed"),
355        ] {
356            assert_eq!(status.as_str(), expected);
357            assert_eq!(status.to_string(), expected);
358            assert_eq!(
359                serde_json::from_value::<CacheRefreshAttemptStatus>(serde_json::json!(expected))
360                    .expect("deserialize refresh-attempt status"),
361                status
362            );
363            assert_eq!(
364                serde_json::to_value(status).expect("serialize refresh-attempt status"),
365                serde_json::json!(expected)
366            );
367        }
368        assert!(
369            serde_json::from_value::<CacheRefreshAttemptStatus>(serde_json::json!("unknown"))
370                .is_err()
371        );
372        for (status, expected) in [
373            (CacheHeaderStatus::Readable, "readable"),
374            (CacheHeaderStatus::Invalid, "invalid"),
375        ] {
376            assert_eq!(status.as_str(), expected);
377            assert_eq!(
378                serde_json::to_value(status).expect("serialize cache header status"),
379                serde_json::json!(expected)
380            );
381        }
382        for (status, expected) in [
383            (CacheAgeStatus::Fresh, "fresh"),
384            (CacheAgeStatus::Stale, "stale"),
385            (CacheAgeStatus::Unmanaged, "unmanaged"),
386            (CacheAgeStatus::Unknown, "unknown"),
387        ] {
388            assert_eq!(status.as_str(), expected);
389            assert_eq!(
390                serde_json::to_value(status).expect("serialize cache age status"),
391                serde_json::json!(expected)
392            );
393        }
394        for (policy, expected) in [
395            (CacheRecoveryPolicy::Automatic, "automatic"),
396            (CacheRecoveryPolicy::Explicit, "explicit"),
397            (CacheRecoveryPolicy::MissingOnly, "missing_only"),
398            (CacheRecoveryPolicy::Unknown, "unknown"),
399        ] {
400            assert_eq!(policy.as_str(), expected);
401            assert_eq!(
402                serde_json::to_value(policy).expect("serialize cache recovery policy"),
403                serde_json::json!(expected)
404            );
405        }
406        for (status, expected) in [
407            (CacheRefreshLockStatus::Active, "active"),
408            (CacheRefreshLockStatus::Available, "available"),
409            (CacheRefreshLockStatus::Invalid, "invalid"),
410        ] {
411            assert_eq!(status.as_str(), expected);
412            assert_eq!(
413                serde_json::to_value(status).expect("serialize refresh-lock status"),
414                serde_json::json!(expected)
415            );
416        }
417    }
418}
419
420///
421/// SnapshotRefreshAttemptStatus
422///
423/// Report-safe lifecycle evidence projected after a family validates its attempt sidecar.
424///
425
426#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
427pub struct SnapshotRefreshAttemptStatus {
428    /// Current lifecycle status.
429    pub status: CacheRefreshAttemptStatus,
430    /// Attempt start timestamp.
431    pub started_at: String,
432    /// Latest attempt update timestamp.
433    pub updated_at: String,
434    /// Requested page size.
435    pub page_size: u32,
436    /// Successfully retained pages.
437    pub pages_fetched: u32,
438    /// Successfully retained rows.
439    pub rows_fetched: usize,
440    /// Latest exclusive cursor.
441    pub last_cursor: Option<String>,
442    /// Terminal failure evidence.
443    pub last_error: Option<String>,
444}