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::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, 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    #[cfg(any(feature = "host", test))]
76    pub(crate) fn from_label(label: &str) -> Option<Self> {
77        match label {
78            "running" => Some(Self::Running),
79            "complete" => Some(Self::Complete),
80            "failed" => Some(Self::Failed),
81            _ => None,
82        }
83    }
84}
85
86impl fmt::Display for CacheRefreshAttemptStatus {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter.write_str(self.as_str())
89    }
90}
91
92///
93/// CacheHeaderStatus
94///
95/// Generic header-integrity classification for one complete cache file.
96///
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99#[serde(rename_all = "snake_case")]
100pub enum CacheHeaderStatus {
101    /// The generic cache header is readable.
102    Readable,
103    /// The generic cache header cannot be read or parsed.
104    Invalid,
105}
106
107impl CacheHeaderStatus {
108    /// Return the stable serialized status label.
109    #[must_use]
110    pub const fn as_str(self) -> &'static str {
111        match self {
112            Self::Readable => "readable",
113            Self::Invalid => "invalid",
114        }
115    }
116}
117
118///
119/// CacheAgeStatus
120///
121/// Caller-relative age classification for one complete cache file.
122///
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
125#[serde(rename_all = "snake_case")]
126pub enum CacheAgeStatus {
127    /// The cache is within its registered family stale threshold.
128    Fresh,
129    /// The cache is older than its registered family stale threshold.
130    Stale,
131    /// The cache has a readable age but no registered family stale threshold.
132    Unmanaged,
133    /// The cache age cannot be calculated from its generic timestamp evidence.
134    Unknown,
135}
136
137impl CacheAgeStatus {
138    /// Return the stable serialized status label.
139    #[must_use]
140    pub const fn as_str(self) -> &'static str {
141        match self {
142            Self::Fresh => "fresh",
143            Self::Stale => "stale",
144            Self::Unmanaged => "unmanaged",
145            Self::Unknown => "unknown",
146        }
147    }
148}
149
150///
151/// CacheRecoveryPolicy
152///
153/// Owner policy for replacing recoverable invalid content at a canonical cache path.
154///
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
157#[serde(rename_all = "snake_case")]
158pub enum CacheRecoveryPolicy {
159    /// An ordinary owner read-through replaces recoverable invalid content.
160    Automatic,
161    /// Recovery requires an explicitly selected refresh operation.
162    Explicit,
163    /// Ordinary read-through creates a missing cache but does not replace invalid content.
164    MissingOnly,
165    /// The path does not identify a current canonical cache owner.
166    Unknown,
167}
168
169impl CacheRecoveryPolicy {
170    /// Return the stable serialized policy label.
171    #[must_use]
172    pub const fn as_str(self) -> &'static str {
173        match self {
174            Self::Automatic => "automatic",
175            Self::Explicit => "explicit",
176            Self::MissingOnly => "missing_only",
177            Self::Unknown => "unknown",
178        }
179    }
180}
181
182///
183/// CacheRefreshLockStatus
184///
185/// Generic age or validity classification for one refresh lock.
186///
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum CacheRefreshLockStatus {
191    /// The lock is within the stale threshold recorded by its owner.
192    Active,
193    /// The lock is older than the stale threshold recorded by its owner.
194    Stale,
195    /// The lock is unreadable, malformed, or future-dated.
196    Invalid,
197}
198
199impl CacheRefreshLockStatus {
200    /// Return the stable serialized status label.
201    #[must_use]
202    pub const fn as_str(self) -> &'static str {
203        match self {
204            Self::Active => "active",
205            Self::Stale => "stale",
206            Self::Invalid => "invalid",
207        }
208    }
209}
210
211///
212/// CacheStatusRequest
213///
214/// Local cache-root inspection request with a caller-supplied observation time.
215///
216
217#[derive(Clone, Debug, Eq, PartialEq)]
218pub struct CacheStatusRequest {
219    /// User-level cache root to inspect.
220    pub cache_root: PathBuf,
221    /// Observation time used to calculate cache ages.
222    pub now_unix_secs: u64,
223}
224
225impl CacheStatusRequest {
226    /// Construct a local cache-status request.
227    #[must_use]
228    pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
229        Self {
230            cache_root: cache_root.into(),
231            now_unix_secs,
232        }
233    }
234}
235
236///
237/// CacheStatusReport
238///
239/// Bounded local inventory of known complete caches and refresh locks.
240///
241
242#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
243pub struct CacheStatusReport {
244    /// Cache-status report schema version.
245    pub schema_version: u32,
246    /// Inspected user-level cache root.
247    pub cache_root: String,
248    /// UTC timestamp at which local inspection was requested.
249    pub inspected_at: String,
250    /// Whether the cache root existed.
251    pub cache_root_found: bool,
252    /// Maximum number of cache and refresh-lock files inspected in one report.
253    pub scan_limit: usize,
254    /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
255    pub truncated: bool,
256    /// Whether the generic inventory performed family-specific semantic validation.
257    pub family_validation_performed: bool,
258    /// Number of cache rows returned.
259    pub cache_count: usize,
260    /// Number of caches with readable generic headers.
261    pub readable_header_count: usize,
262    /// Number of caches with unreadable or malformed generic headers.
263    pub invalid_header_count: usize,
264    /// Number of caches fresh under an explicit family policy.
265    pub fresh_count: usize,
266    /// Number of caches stale under an explicit family policy.
267    pub stale_count: usize,
268    /// Number of readable caches whose family has no registered age policy.
269    pub unmanaged_age_count: usize,
270    /// Number of caches whose age cannot be calculated from generic evidence.
271    pub unknown_age_count: usize,
272    /// Sum of filesystem sizes for returned cache files.
273    pub total_size_bytes: u64,
274    /// Canonically path-ordered cache rows.
275    pub caches: Vec<CacheStatusRow>,
276    /// Number of refresh-lock rows returned.
277    pub refresh_lock_count: usize,
278    /// Number of locks still active under their recorded stale policy.
279    pub active_refresh_lock_count: usize,
280    /// Number of locks older than their recorded stale policy.
281    pub stale_refresh_lock_count: usize,
282    /// Number of unreadable, malformed, or future-dated locks.
283    pub invalid_refresh_lock_count: usize,
284    /// Sum of filesystem sizes for returned refresh-lock files.
285    pub refresh_lock_size_bytes: u64,
286    /// Canonically path-ordered refresh-lock rows.
287    pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
288}
289
290///
291/// CacheStatusRow
292///
293/// Generic local metadata and caller-relative age for one complete cache file.
294///
295
296#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
297pub struct CacheStatusRow {
298    /// Stable component label inferred from cache identity or canonical path.
299    pub component: String,
300    /// Absolute cache-file path.
301    pub cache_path: String,
302    /// Cache-root-relative path.
303    pub relative_path: String,
304    /// Generic cache-header integrity without family-specific semantic validation.
305    pub header_status: CacheHeaderStatus,
306    /// Caller-relative age classification kept separate from header integrity.
307    pub age_status: CacheAgeStatus,
308    /// Owner policy for recovering invalid content at this canonical path.
309    pub recovery_policy: CacheRecoveryPolicy,
310    /// Serialized cache schema version when readable.
311    pub schema_version: Option<u32>,
312    /// Serialized network identity when present.
313    pub network: Option<String>,
314    /// Cache collection timestamp when readable.
315    pub fetched_at: Option<String>,
316    /// Caller-relative age when the timestamp is valid and not in the future.
317    pub age_seconds: Option<u64>,
318    /// Family age threshold when one is explicitly defined.
319    pub stale_after_seconds: Option<u64>,
320    /// Filesystem size of this cache file.
321    pub size_bytes: u64,
322    /// Generic header or timestamp inspection error; family-specific validation is separate.
323    pub inspection_error: Option<String>,
324}
325
326///
327/// CacheRefreshLockStatusRow
328///
329/// Local identity, ownership, age, and stale policy for one refresh lock.
330///
331
332#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
333pub struct CacheRefreshLockStatusRow {
334    /// Stable component label inferred from the recorded cache target.
335    pub component: String,
336    /// Absolute refresh-lock path.
337    pub refresh_lock_path: String,
338    /// Cache-root-relative refresh-lock path.
339    pub relative_path: String,
340    /// Generic lock age or validity classification.
341    pub status: CacheRefreshLockStatus,
342    /// Serialized refresh-lock schema version when readable.
343    pub schema_version: Option<u32>,
344    /// Serialized network identity when readable.
345    pub network: Option<String>,
346    /// Operating-system process id recorded by the lock owner when readable.
347    pub pid: Option<u32>,
348    /// Raw Unix-millisecond acquisition time when readable.
349    pub started_at_unix_ms: Option<u64>,
350    /// UTC acquisition timestamp when readable.
351    pub started_at: Option<String>,
352    /// Caller-relative lock age when the timestamp is not in the future.
353    pub age_seconds: Option<u64>,
354    /// Stale threshold recorded by the lock owner.
355    pub stale_after_seconds: Option<u64>,
356    /// Cache target recorded by the lock owner when readable.
357    pub target_path: Option<String>,
358    /// Filesystem size of this refresh-lock file.
359    pub size_bytes: u64,
360    /// Lock parse, shape, or timestamp error.
361    pub error: Option<String>,
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn typed_statuses_keep_stable_json_labels() {
370        for (status, expected) in [
371            (CacheValidationStatus::Valid, "ok"),
372            (CacheValidationStatus::Invalid, "invalid"),
373        ] {
374            assert_eq!(status.as_str(), expected);
375            assert_eq!(status.to_string(), expected);
376            assert_eq!(
377                serde_json::to_value(status).expect("serialize cache validation status"),
378                serde_json::json!(expected)
379            );
380        }
381        for (status, expected) in [
382            (CacheRefreshAttemptStatus::Running, "running"),
383            (CacheRefreshAttemptStatus::Complete, "complete"),
384            (CacheRefreshAttemptStatus::Failed, "failed"),
385        ] {
386            assert_eq!(status.as_str(), expected);
387            assert_eq!(status.to_string(), expected);
388            assert_eq!(
389                CacheRefreshAttemptStatus::from_label(expected),
390                Some(status)
391            );
392            assert_eq!(
393                serde_json::to_value(status).expect("serialize refresh-attempt status"),
394                serde_json::json!(expected)
395            );
396        }
397        assert_eq!(CacheRefreshAttemptStatus::from_label("unknown"), None);
398        for (status, expected) in [
399            (CacheHeaderStatus::Readable, "readable"),
400            (CacheHeaderStatus::Invalid, "invalid"),
401        ] {
402            assert_eq!(status.as_str(), expected);
403            assert_eq!(
404                serde_json::to_value(status).expect("serialize cache header status"),
405                serde_json::json!(expected)
406            );
407        }
408        for (status, expected) in [
409            (CacheAgeStatus::Fresh, "fresh"),
410            (CacheAgeStatus::Stale, "stale"),
411            (CacheAgeStatus::Unmanaged, "unmanaged"),
412            (CacheAgeStatus::Unknown, "unknown"),
413        ] {
414            assert_eq!(status.as_str(), expected);
415            assert_eq!(
416                serde_json::to_value(status).expect("serialize cache age status"),
417                serde_json::json!(expected)
418            );
419        }
420        for (policy, expected) in [
421            (CacheRecoveryPolicy::Automatic, "automatic"),
422            (CacheRecoveryPolicy::Explicit, "explicit"),
423            (CacheRecoveryPolicy::MissingOnly, "missing_only"),
424            (CacheRecoveryPolicy::Unknown, "unknown"),
425        ] {
426            assert_eq!(policy.as_str(), expected);
427            assert_eq!(
428                serde_json::to_value(policy).expect("serialize cache recovery policy"),
429                serde_json::json!(expected)
430            );
431        }
432        for (status, expected) in [
433            (CacheRefreshLockStatus::Active, "active"),
434            (CacheRefreshLockStatus::Stale, "stale"),
435            (CacheRefreshLockStatus::Invalid, "invalid"),
436        ] {
437            assert_eq!(status.as_str(), expected);
438            assert_eq!(
439                serde_json::to_value(status).expect("serialize refresh-lock status"),
440                serde_json::json!(expected)
441            );
442        }
443    }
444}