Skip to main content

sqry_daemon/workspace/
state.rs

1//! Workspace state machine and key types.
2//!
3//! Corresponds to Task 6 Step 1 of the sqryd plan:
4//!
5//! - [`WorkspaceState`] — the six-state workspace lifecycle enum (A2 §G.5,
6//!   §G.7). **Moved to `sqry-daemon-protocol` in Phase 8c U1** so the
7//!   wire-type [`crate::ipc::protocol::ResponseMeta`] can carry a canonical
8//!   `workspace_state` field without the leaf protocol crate taking a dep
9//!   on `sqry-daemon`. Re-exported here so existing call sites continue to
10//!   compile. Stored on [`crate::workspace::LoadedWorkspace`] as an
11//!   [`AtomicU8`]; the `#[repr(u8)]` discriminant makes the store / load
12//!   round-trip lossless and gives cheap exhaustive match arms.
13//! - [`WorkspaceKey`] — the identity used to dedup workspaces in
14//!   [`crate::workspace::WorkspaceManager`]. Composed of the absolute
15//!   `index_root`, the [`ProjectRootMode`] (so the same repo opened with
16//!   different modes gets distinct cache entries), and a config
17//!   fingerprint (so a meaningful config change forces a fresh load).
18//! - [`OldGraphToken`] — opaque handle used by the admission map to key
19//!   retained old graphs. Never serialised; values are process-local.
20
21use std::{
22    path::PathBuf,
23    sync::atomic::{AtomicU64, Ordering},
24};
25
26use serde::{Deserialize, Serialize};
27use sqry_core::project::ProjectRootMode;
28
29// ---------------------------------------------------------------------------
30// WorkspaceState — re-exported from the leaf protocol crate.
31// ---------------------------------------------------------------------------
32
33pub use sqry_daemon_protocol::protocol::WorkspaceState;
34
35// ---------------------------------------------------------------------------
36// WorkspaceKey
37// ---------------------------------------------------------------------------
38
39/// Composite identity for a loaded workspace.
40///
41/// Two workspaces with the same [`Self::index_root`] but different
42/// [`Self::root_mode`] or [`Self::config_fingerprint`] are distinct cache
43/// entries — this prevents cache collisions when the same repo is opened
44/// with different client configurations.
45///
46/// `index_root` is always the canonical absolute path (caller
47/// responsibility; [`Self::new`] does not canonicalise because a path may
48/// not exist on disk yet at the moment a key is synthesised).
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct WorkspaceKey {
51    /// Canonical absolute path to the workspace root directory.
52    pub index_root: PathBuf,
53
54    /// How the project root was determined for this workspace.
55    pub root_mode: ProjectRootMode,
56
57    /// 64-bit fingerprint of the config values that materially affect
58    /// the graph (plugin selection, cost tiering, macro expansion
59    /// toggles, etc). Callers compute the fingerprint deterministically
60    /// from the client-declared options; an unchanged fingerprint
61    /// means the daemon is free to return the cached graph.
62    pub config_fingerprint: u64,
63}
64
65impl WorkspaceKey {
66    /// Construct a new key. Caller is responsible for passing a canonical
67    /// absolute path.
68    #[must_use]
69    pub fn new(index_root: PathBuf, root_mode: ProjectRootMode, config_fingerprint: u64) -> Self {
70        Self {
71            index_root,
72            root_mode,
73            config_fingerprint,
74        }
75    }
76}
77
78impl std::fmt::Display for WorkspaceKey {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(
81            f,
82            "{}[{}@{:016x}]",
83            self.index_root.display(),
84            self.root_mode,
85            self.config_fingerprint,
86        )
87    }
88}
89
90// ---------------------------------------------------------------------------
91// OldGraphToken
92// ---------------------------------------------------------------------------
93
94/// Opaque token used by [`crate::workspace::admission::AdmissionState::retained_old`]
95/// to key entries for retained old graphs.
96///
97/// The token is a monotonic per-process counter sourced from a single
98/// [`AtomicU64`]. Uniqueness is guaranteed for the lifetime of the daemon
99/// process (2^64 is effectively inexhaustible at daemon-scale rates).
100/// Tokens are never persisted or serialised across restarts.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102pub struct OldGraphToken(u64);
103
104impl OldGraphToken {
105    /// Mint a fresh token. Thread-safe — concurrent callers always
106    /// receive distinct values.
107    pub fn new() -> Self {
108        static COUNTER: AtomicU64 = AtomicU64::new(1);
109        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
110    }
111
112    /// Inspect the raw token value (useful for tracing).
113    #[must_use]
114    pub const fn raw(self) -> u64 {
115        self.0
116    }
117}
118
119impl Default for OldGraphToken {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl std::fmt::Display for OldGraphToken {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "OldGraphToken({})", self.0)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn state_round_trips_via_discriminant() {
137        for &s in &[
138            WorkspaceState::Unloaded,
139            WorkspaceState::Loading,
140            WorkspaceState::Loaded,
141            WorkspaceState::Rebuilding,
142            WorkspaceState::Evicted,
143            WorkspaceState::Failed,
144        ] {
145            assert_eq!(WorkspaceState::from_u8(s.as_u8()), Some(s), "{s}");
146        }
147    }
148
149    #[test]
150    fn state_from_out_of_range_is_none() {
151        assert_eq!(WorkspaceState::from_u8(6), None);
152        assert_eq!(WorkspaceState::from_u8(255), None);
153    }
154
155    #[test]
156    fn state_is_serving_matches_a2_table() {
157        assert!(!WorkspaceState::Unloaded.is_serving());
158        assert!(!WorkspaceState::Loading.is_serving());
159        assert!(WorkspaceState::Loaded.is_serving());
160        assert!(WorkspaceState::Rebuilding.is_serving());
161        assert!(!WorkspaceState::Evicted.is_serving());
162        assert!(WorkspaceState::Failed.is_serving());
163    }
164
165    #[test]
166    fn key_distinguishes_root_mode_and_fingerprint() {
167        let a = WorkspaceKey::new(
168            PathBuf::from("/repos/example"),
169            ProjectRootMode::GitRoot,
170            0x1234_5678_9abc_def0,
171        );
172        let b = WorkspaceKey::new(
173            PathBuf::from("/repos/example"),
174            ProjectRootMode::WorkspaceFolder,
175            0x1234_5678_9abc_def0,
176        );
177        let c = WorkspaceKey::new(
178            PathBuf::from("/repos/example"),
179            ProjectRootMode::GitRoot,
180            0xdead_beef_dead_beef,
181        );
182        assert_ne!(a, b, "different root_mode must be different keys");
183        assert_ne!(a, c, "different fingerprint must be different keys");
184        assert_eq!(a, a.clone(), "same components compare equal");
185    }
186
187    #[test]
188    fn token_is_monotonic_and_unique() {
189        let a = OldGraphToken::new();
190        let b = OldGraphToken::new();
191        let c = OldGraphToken::new();
192        assert!(a.raw() < b.raw());
193        assert!(b.raw() < c.raw());
194        assert_ne!(a, b);
195        assert_ne!(b, c);
196    }
197}