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/// CacheFileStatus
49///
50/// Generic freshness or validity classification for one complete cache file.
51///
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CacheFileStatus {
56    /// The cache is within its registered family stale threshold.
57    Fresh,
58    /// The cache is older than its registered family stale threshold.
59    Stale,
60    /// The cache has a readable age but no registered family stale threshold.
61    Unmanaged,
62    /// The generic cache header or timestamp is unreadable or invalid.
63    Invalid,
64}
65
66impl CacheFileStatus {
67    /// Return the stable serialized status label.
68    #[must_use]
69    pub const fn as_str(self) -> &'static str {
70        match self {
71            Self::Fresh => "fresh",
72            Self::Stale => "stale",
73            Self::Unmanaged => "unmanaged",
74            Self::Invalid => "invalid",
75        }
76    }
77}
78
79///
80/// CacheRefreshLockStatus
81///
82/// Generic age or validity classification for one refresh lock.
83///
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
86#[serde(rename_all = "snake_case")]
87pub enum CacheRefreshLockStatus {
88    /// The lock is within the stale threshold recorded by its owner.
89    Active,
90    /// The lock is older than the stale threshold recorded by its owner.
91    Stale,
92    /// The lock is unreadable, malformed, or future-dated.
93    Invalid,
94}
95
96impl CacheRefreshLockStatus {
97    /// Return the stable serialized status label.
98    #[must_use]
99    pub const fn as_str(self) -> &'static str {
100        match self {
101            Self::Active => "active",
102            Self::Stale => "stale",
103            Self::Invalid => "invalid",
104        }
105    }
106}
107
108///
109/// CacheStatusRequest
110///
111/// Local cache-root inspection request with a caller-supplied observation time.
112///
113
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct CacheStatusRequest {
116    /// User-level cache root to inspect.
117    pub cache_root: PathBuf,
118    /// Observation time used to calculate cache ages.
119    pub now_unix_secs: u64,
120}
121
122impl CacheStatusRequest {
123    /// Construct a local cache-status request.
124    #[must_use]
125    pub fn new(cache_root: impl Into<PathBuf>, now_unix_secs: u64) -> Self {
126        Self {
127            cache_root: cache_root.into(),
128            now_unix_secs,
129        }
130    }
131}
132
133///
134/// CacheStatusReport
135///
136/// Bounded local inventory of known complete caches and refresh locks.
137///
138
139#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
140pub struct CacheStatusReport {
141    /// Cache-status report schema version.
142    pub schema_version: u32,
143    /// Inspected user-level cache root.
144    pub cache_root: String,
145    /// UTC timestamp at which local inspection was requested.
146    pub inspected_at: String,
147    /// Whether the cache root existed.
148    pub cache_root_found: bool,
149    /// Maximum number of cache and refresh-lock files inspected in one report.
150    pub scan_limit: usize,
151    /// Whether additional cache or refresh-lock candidates existed beyond the scan limit.
152    pub truncated: bool,
153    /// Number of cache rows returned.
154    pub cache_count: usize,
155    /// Number of caches fresh under an explicit family policy.
156    pub fresh_count: usize,
157    /// Number of caches stale under an explicit family policy.
158    pub stale_count: usize,
159    /// Number of readable caches whose family has no registered age policy.
160    pub unmanaged_count: usize,
161    /// Number of files whose generic cache header or timestamp was invalid.
162    pub invalid_count: usize,
163    /// Sum of filesystem sizes for returned cache files.
164    pub total_size_bytes: u64,
165    /// Canonically path-ordered cache rows.
166    pub caches: Vec<CacheStatusRow>,
167    /// Number of refresh-lock rows returned.
168    pub refresh_lock_count: usize,
169    /// Number of locks still active under their recorded stale policy.
170    pub active_refresh_lock_count: usize,
171    /// Number of locks older than their recorded stale policy.
172    pub stale_refresh_lock_count: usize,
173    /// Number of unreadable, malformed, or future-dated locks.
174    pub invalid_refresh_lock_count: usize,
175    /// Sum of filesystem sizes for returned refresh-lock files.
176    pub refresh_lock_size_bytes: u64,
177    /// Canonically path-ordered refresh-lock rows.
178    pub refresh_locks: Vec<CacheRefreshLockStatusRow>,
179}
180
181///
182/// CacheStatusRow
183///
184/// Generic local metadata and caller-relative age for one complete cache file.
185///
186
187#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
188pub struct CacheStatusRow {
189    /// Stable component label inferred from cache identity or canonical path.
190    pub component: String,
191    /// Absolute cache-file path.
192    pub cache_path: String,
193    /// Cache-root-relative path.
194    pub relative_path: String,
195    /// Generic freshness or validity classification.
196    pub status: CacheFileStatus,
197    /// Serialized cache schema version when readable.
198    pub schema_version: Option<u32>,
199    /// Serialized network identity when present.
200    pub network: Option<String>,
201    /// Cache collection timestamp when readable.
202    pub fetched_at: Option<String>,
203    /// Caller-relative age when the timestamp is valid and not in the future.
204    pub age_seconds: Option<u64>,
205    /// Family age threshold when one is explicitly defined.
206    pub stale_after_seconds: Option<u64>,
207    /// Filesystem size of this cache file.
208    pub size_bytes: u64,
209    /// Generic header or timestamp error; family-specific validation is separate.
210    pub error: Option<String>,
211}
212
213///
214/// CacheRefreshLockStatusRow
215///
216/// Local identity, ownership, age, and stale policy for one refresh lock.
217///
218
219#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
220pub struct CacheRefreshLockStatusRow {
221    /// Stable component label inferred from the recorded cache target.
222    pub component: String,
223    /// Absolute refresh-lock path.
224    pub refresh_lock_path: String,
225    /// Cache-root-relative refresh-lock path.
226    pub relative_path: String,
227    /// Generic lock age or validity classification.
228    pub status: CacheRefreshLockStatus,
229    /// Serialized refresh-lock schema version when readable.
230    pub schema_version: Option<u32>,
231    /// Serialized network identity when readable.
232    pub network: Option<String>,
233    /// Operating-system process id recorded by the lock owner when readable.
234    pub pid: Option<u32>,
235    /// Raw Unix-millisecond acquisition time when readable.
236    pub started_at_unix_ms: Option<u64>,
237    /// UTC acquisition timestamp when readable.
238    pub started_at: Option<String>,
239    /// Caller-relative lock age when the timestamp is not in the future.
240    pub age_seconds: Option<u64>,
241    /// Stale threshold recorded by the lock owner.
242    pub stale_after_seconds: Option<u64>,
243    /// Cache target recorded by the lock owner when readable.
244    pub target_path: Option<String>,
245    /// Filesystem size of this refresh-lock file.
246    pub size_bytes: u64,
247    /// Lock parse, shape, or timestamp error.
248    pub error: Option<String>,
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn typed_statuses_keep_the_existing_json_labels() {
257        for (status, expected) in [
258            (CacheValidationStatus::Valid, "ok"),
259            (CacheValidationStatus::Invalid, "invalid"),
260        ] {
261            assert_eq!(status.as_str(), expected);
262            assert_eq!(status.to_string(), expected);
263            assert_eq!(
264                serde_json::to_value(status).expect("serialize cache validation status"),
265                serde_json::json!(expected)
266            );
267        }
268        for (status, expected) in [
269            (CacheFileStatus::Fresh, "fresh"),
270            (CacheFileStatus::Stale, "stale"),
271            (CacheFileStatus::Unmanaged, "unmanaged"),
272            (CacheFileStatus::Invalid, "invalid"),
273        ] {
274            assert_eq!(status.as_str(), expected);
275            assert_eq!(
276                serde_json::to_value(status).expect("serialize cache status"),
277                serde_json::json!(expected)
278            );
279        }
280        for (status, expected) in [
281            (CacheRefreshLockStatus::Active, "active"),
282            (CacheRefreshLockStatus::Stale, "stale"),
283            (CacheRefreshLockStatus::Invalid, "invalid"),
284        ] {
285            assert_eq!(status.as_str(), expected);
286            assert_eq!(
287                serde_json::to_value(status).expect("serialize refresh-lock status"),
288                serde_json::json!(expected)
289            );
290        }
291    }
292}