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 file, age,
6//! and freshness 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 = 2;
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/// CacheFileStatus
94///
95/// Generic freshness or validity classification for one complete cache file.
96///
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99#[serde(rename_all = "snake_case")]
100pub enum CacheFileStatus {
101    /// The cache is within its registered family stale threshold.
102    Fresh,
103    /// The cache is older than its registered family stale threshold.
104    Stale,
105    /// The cache has a readable age but no registered family stale threshold.
106    Unmanaged,
107    /// The generic cache header or timestamp is unreadable or invalid.
108    Invalid,
109}
110
111impl CacheFileStatus {
112    /// Return the stable serialized status label.
113    #[must_use]
114    pub const fn as_str(self) -> &'static str {
115        match self {
116            Self::Fresh => "fresh",
117            Self::Stale => "stale",
118            Self::Unmanaged => "unmanaged",
119            Self::Invalid => "invalid",
120        }
121    }
122}
123
124///
125/// CacheRefreshLockStatus
126///
127/// Generic age or validity classification for one refresh lock.
128///
129
130#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
131#[serde(rename_all = "snake_case")]
132pub enum CacheRefreshLockStatus {
133    /// The lock is within the stale threshold recorded by its owner.
134    Active,
135    /// The lock is older than the stale threshold recorded by its owner.
136    Stale,
137    /// The lock is unreadable, malformed, or future-dated.
138    Invalid,
139}
140
141impl CacheRefreshLockStatus {
142    /// Return the stable serialized status label.
143    #[must_use]
144    pub const fn as_str(self) -> &'static str {
145        match self {
146            Self::Active => "active",
147            Self::Stale => "stale",
148            Self::Invalid => "invalid",
149        }
150    }
151}
152
153///
154/// CacheStatusRequest
155///
156/// Local cache-root inspection request with a caller-supplied observation time.
157///
158
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct CacheStatusRequest {
161    /// User-level cache root to inspect.
162    pub cache_root: PathBuf,
163    /// Observation time used to calculate cache ages.
164    pub now_unix_secs: u64,
165}
166
167impl CacheStatusRequest {
168    /// Construct a local cache-status request.
169    #[must_use]
170    pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
171        Self {
172            cache_root: cache_root.into(),
173            now_unix_secs,
174        }
175    }
176}
177
178///
179/// CacheStatusReport
180///
181/// Bounded local inventory of known complete caches and refresh locks.
182///
183
184#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
185pub struct CacheStatusReport {
186    /// Cache-status report schema version.
187    pub schema_version: u32,
188    /// Inspected user-level cache root.
189    pub cache_root: String,
190    /// UTC timestamp at which local inspection was requested.
191    pub inspected_at: String,
192    /// Whether the cache root existed.
193    pub cache_root_found: bool,
194    /// Maximum number of cache and refresh-lock files inspected in one report.
195    pub scan_limit: usize,
196    /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
197    pub truncated: bool,
198    /// Number of cache rows returned.
199    pub cache_count: usize,
200    /// Number of caches fresh under an explicit family policy.
201    pub fresh_count: usize,
202    /// Number of caches stale under an explicit family policy.
203    pub stale_count: usize,
204    /// Number of readable caches whose family has no registered age policy.
205    pub unmanaged_count: usize,
206    /// Number of files whose generic cache header or timestamp was invalid.
207    pub invalid_count: usize,
208    /// Sum of filesystem sizes for returned cache files.
209    pub total_size_bytes: u64,
210    /// Canonically path-ordered cache rows.
211    pub caches: Vec<CacheStatusRow>,
212    /// Number of refresh-lock rows returned.
213    pub refresh_lock_count: usize,
214    /// Number of locks still active under their recorded stale policy.
215    pub active_refresh_lock_count: usize,
216    /// Number of locks older than their recorded stale policy.
217    pub stale_refresh_lock_count: usize,
218    /// Number of unreadable, malformed, or future-dated locks.
219    pub invalid_refresh_lock_count: usize,
220    /// Sum of filesystem sizes for returned refresh-lock files.
221    pub refresh_lock_size_bytes: u64,
222    /// Canonically path-ordered refresh-lock rows.
223    pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
224}
225
226///
227/// CacheStatusRow
228///
229/// Generic local metadata and caller-relative age for one complete cache file.
230///
231
232#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
233pub struct CacheStatusRow {
234    /// Stable component label inferred from cache identity or canonical path.
235    pub component: String,
236    /// Absolute cache-file path.
237    pub cache_path: String,
238    /// Cache-root-relative path.
239    pub relative_path: String,
240    /// Generic freshness or validity classification.
241    pub status: CacheFileStatus,
242    /// Serialized cache schema version when readable.
243    pub schema_version: Option<u32>,
244    /// Serialized network identity when present.
245    pub network: Option<String>,
246    /// Cache collection timestamp when readable.
247    pub fetched_at: Option<String>,
248    /// Caller-relative age when the timestamp is valid and not in the future.
249    pub age_seconds: Option<u64>,
250    /// Family age threshold when one is explicitly defined.
251    pub stale_after_seconds: Option<u64>,
252    /// Filesystem size of this cache file.
253    pub size_bytes: u64,
254    /// Generic header or timestamp error; family-specific validation is separate.
255    pub error: Option<String>,
256}
257
258///
259/// CacheRefreshLockStatusRow
260///
261/// Local identity, ownership, age, and stale policy for one refresh lock.
262///
263
264#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
265pub struct CacheRefreshLockStatusRow {
266    /// Stable component label inferred from the recorded cache target.
267    pub component: String,
268    /// Absolute refresh-lock path.
269    pub refresh_lock_path: String,
270    /// Cache-root-relative refresh-lock path.
271    pub relative_path: String,
272    /// Generic lock age or validity classification.
273    pub status: CacheRefreshLockStatus,
274    /// Serialized refresh-lock schema version when readable.
275    pub schema_version: Option<u32>,
276    /// Serialized network identity when readable.
277    pub network: Option<String>,
278    /// Operating-system process id recorded by the lock owner when readable.
279    pub pid: Option<u32>,
280    /// Raw Unix-millisecond acquisition time when readable.
281    pub started_at_unix_ms: Option<u64>,
282    /// UTC acquisition timestamp when readable.
283    pub started_at: Option<String>,
284    /// Caller-relative lock age when the timestamp is not in the future.
285    pub age_seconds: Option<u64>,
286    /// Stale threshold recorded by the lock owner.
287    pub stale_after_seconds: Option<u64>,
288    /// Cache target recorded by the lock owner when readable.
289    pub target_path: Option<String>,
290    /// Filesystem size of this refresh-lock file.
291    pub size_bytes: u64,
292    /// Lock parse, shape, or timestamp error.
293    pub error: Option<String>,
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn typed_statuses_keep_the_existing_json_labels() {
302        for (status, expected) in [
303            (CacheValidationStatus::Valid, "ok"),
304            (CacheValidationStatus::Invalid, "invalid"),
305        ] {
306            assert_eq!(status.as_str(), expected);
307            assert_eq!(status.to_string(), expected);
308            assert_eq!(
309                serde_json::to_value(status).expect("serialize cache validation status"),
310                serde_json::json!(expected)
311            );
312        }
313        for (status, expected) in [
314            (CacheRefreshAttemptStatus::Running, "running"),
315            (CacheRefreshAttemptStatus::Complete, "complete"),
316            (CacheRefreshAttemptStatus::Failed, "failed"),
317        ] {
318            assert_eq!(status.as_str(), expected);
319            assert_eq!(status.to_string(), expected);
320            assert_eq!(
321                CacheRefreshAttemptStatus::from_label(expected),
322                Some(status)
323            );
324            assert_eq!(
325                serde_json::to_value(status).expect("serialize refresh-attempt status"),
326                serde_json::json!(expected)
327            );
328        }
329        assert_eq!(CacheRefreshAttemptStatus::from_label("unknown"), None);
330        for (status, expected) in [
331            (CacheFileStatus::Fresh, "fresh"),
332            (CacheFileStatus::Stale, "stale"),
333            (CacheFileStatus::Unmanaged, "unmanaged"),
334            (CacheFileStatus::Invalid, "invalid"),
335        ] {
336            assert_eq!(status.as_str(), expected);
337            assert_eq!(
338                serde_json::to_value(status).expect("serialize cache status"),
339                serde_json::json!(expected)
340            );
341        }
342        for (status, expected) in [
343            (CacheRefreshLockStatus::Active, "active"),
344            (CacheRefreshLockStatus::Stale, "stale"),
345            (CacheRefreshLockStatus::Invalid, "invalid"),
346        ] {
347            assert_eq!(status.as_str(), expected);
348            assert_eq!(
349                serde_json::to_value(status).expect("serialize refresh-lock status"),
350                serde_json::json!(expected)
351            );
352        }
353    }
354}