Skip to main content

evolve_core/
ids.rs

1//! Strongly-typed identifier newtypes used across the framework.
2//!
3//! Each entity (project, agent config, experiment, session, signal) gets its own
4//! UUID-backed newtype so that mixing them at a call site is a compile error rather
5//! than a runtime bug. Adapters identify themselves by a stable string instead of
6//! a UUID, since adapter identity is part of the public protocol.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use uuid::Uuid;
11
12macro_rules! uuid_id {
13    ($(#[$attr:meta])* $name:ident) => {
14        $(#[$attr])*
15        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16        pub struct $name(pub Uuid);
17
18        impl $name {
19            /// Generate a fresh random identifier.
20            pub fn new() -> Self {
21                Self(Uuid::new_v4())
22            }
23
24            /// Construct from an existing UUID (useful when loading from storage).
25            pub fn from_uuid(uuid: Uuid) -> Self {
26                Self(uuid)
27            }
28
29            /// Borrow the inner UUID.
30            pub fn as_uuid(&self) -> &Uuid {
31                &self.0
32            }
33        }
34
35        impl Default for $name {
36            fn default() -> Self {
37                Self::new()
38            }
39        }
40
41        impl fmt::Display for $name {
42            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43                self.0.fmt(f)
44            }
45        }
46    };
47}
48
49uuid_id!(
50    /// Identifies a project that Evolve is managing.
51    ProjectId
52);
53uuid_id!(
54    /// Identifies an [`AgentConfig`](crate::agent_config::AgentConfig) row in storage.
55    ConfigId
56);
57uuid_id!(
58    /// Identifies an in-flight or completed champion-vs-challenger experiment.
59    ExperimentId
60);
61uuid_id!(
62    /// Identifies a single user session as recorded by an adapter hook.
63    SessionId
64);
65uuid_id!(
66    /// Identifies a single fitness signal contributed to a session.
67    SignalId
68);
69
70/// Stable string identifier for a registered adapter (e.g., `"claude-code"`,
71/// `"cursor"`, `"aider"`). String-based rather than UUID-based because adapter
72/// identity is part of the public protocol — IDs must round-trip across machines
73/// and across the wire.
74#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
75pub struct AdapterId(String);
76
77impl AdapterId {
78    /// Construct an adapter id from a stable, lowercase, kebab-cased name.
79    pub fn new(name: impl Into<String>) -> Self {
80        Self(name.into())
81    }
82
83    /// Borrow the inner name.
84    pub fn as_str(&self) -> &str {
85        &self.0
86    }
87}
88
89impl fmt::Display for AdapterId {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.write_str(&self.0)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn project_id_roundtrips_through_json() {
101        let id = ProjectId::new();
102        let json = serde_json::to_string(&id).unwrap();
103        let back: ProjectId = serde_json::from_str(&json).unwrap();
104        assert_eq!(id, back);
105    }
106
107    #[test]
108    fn distinct_ids_are_not_equal() {
109        let a = ProjectId::new();
110        let b = ProjectId::new();
111        assert_ne!(a, b);
112    }
113
114    #[test]
115    fn from_uuid_then_as_uuid_roundtrips() {
116        let raw = Uuid::new_v4();
117        let id = SessionId::from_uuid(raw);
118        assert_eq!(id.as_uuid(), &raw);
119    }
120
121    #[test]
122    fn display_matches_inner_uuid() {
123        let raw = Uuid::new_v4();
124        let id = ConfigId::from_uuid(raw);
125        assert_eq!(format!("{id}"), format!("{raw}"));
126    }
127
128    #[test]
129    fn adapter_id_roundtrips() {
130        let id = AdapterId::new("claude-code");
131        let json = serde_json::to_string(&id).unwrap();
132        let back: AdapterId = serde_json::from_str(&json).unwrap();
133        assert_eq!(id, back);
134        assert_eq!(id.as_str(), "claude-code");
135    }
136
137    #[test]
138    fn adapter_id_display_matches_inner_string() {
139        let id = AdapterId::new("aider");
140        assert_eq!(format!("{id}"), "aider");
141    }
142
143    #[test]
144    fn distinct_id_types_are_not_interchangeable() {
145        // This is enforced by the type system — there's nothing to assert at runtime.
146        // The test exists to document the intent: the compiler MUST reject
147        // `let _: ProjectId = ConfigId::new();`. Verified by the fact that this file
148        // compiles only without that line.
149        let _project = ProjectId::new();
150        let _config = ConfigId::new();
151    }
152}