Skip to main content

pulsedb/collective/
types.rs

1//! Type definitions for collectives.
2//!
3//! A **collective** is an isolated namespace for experiences, typically one per project.
4//! Each collective has its own embedding dimension and vector index.
5
6use serde::{Deserialize, Serialize};
7
8use crate::types::{CollectiveId, Timestamp};
9
10/// A collective — an isolated namespace for agent experiences.
11///
12/// Collectives provide multi-tenancy: each project or team gets its own
13/// collective with its own experiences and vector index.
14///
15/// # Fields
16///
17/// - `id` — Unique identifier (UUID v7, time-ordered)
18/// - `name` — Human-readable name (e.g., "my-project")
19/// - `owner_id` — Optional owner for multi-tenant filtering
20/// - `embedding_dimension` — Vector dimension locked at creation (e.g., 384, 768)
21/// - `created_at` / `updated_at` — Lifecycle timestamps
22///
23/// # Serialization
24///
25/// Collectives are serialized with bincode for compact storage in redb.
26/// The `Serialize`/`Deserialize` derives enable this automatically.
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct Collective {
29    /// Unique identifier (UUID v7).
30    pub id: CollectiveId,
31
32    /// Human-readable name.
33    pub name: String,
34
35    /// Optional owner identifier for multi-tenancy.
36    ///
37    /// When set, enables filtering collectives by owner via
38    /// `list_collectives_by_owner()`.
39    pub owner_id: Option<String>,
40
41    /// Embedding vector dimension for this collective.
42    ///
43    /// All experiences in this collective must have embeddings
44    /// with exactly this many dimensions. Locked at creation time.
45    pub embedding_dimension: u16,
46
47    /// When this collective was created.
48    pub created_at: Timestamp,
49
50    /// When this collective was last modified.
51    pub updated_at: Timestamp,
52}
53
54impl Collective {
55    /// Creates a new collective with the given name and embedding dimension.
56    ///
57    /// Sets `created_at` and `updated_at` to the current time.
58    /// The `owner_id` defaults to `None`.
59    pub fn new(name: impl Into<String>, embedding_dimension: u16) -> Self {
60        let now = Timestamp::now();
61        Self {
62            id: CollectiveId::new(),
63            name: name.into(),
64            owner_id: None,
65            embedding_dimension,
66            created_at: now,
67            updated_at: now,
68        }
69    }
70
71    /// Creates a new collective with an owner.
72    pub fn with_owner(
73        name: impl Into<String>,
74        owner_id: impl Into<String>,
75        embedding_dimension: u16,
76    ) -> Self {
77        let mut collective = Self::new(name, embedding_dimension);
78        collective.owner_id = Some(owner_id.into());
79        collective
80    }
81}
82
83/// Statistics for a collective.
84///
85/// Returned by [`PulseDB::get_collective_stats()`](crate::PulseDB::get_collective_stats).
86/// These values are computed on-the-fly from the storage layer, not cached.
87#[derive(Clone, Debug)]
88pub struct CollectiveStats {
89    /// Number of experiences in this collective.
90    pub experience_count: u64,
91    /// Estimated storage size in bytes for this collective's data.
92    pub storage_bytes: u64,
93    /// Timestamp of the oldest experience, if any.
94    pub oldest_experience: Option<Timestamp>,
95    /// Timestamp of the newest experience, if any.
96    pub newest_experience: Option<Timestamp>,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_collective_new() {
105        let collective = Collective::new("test-project", 384);
106        assert_eq!(collective.name, "test-project");
107        assert_eq!(collective.embedding_dimension, 384);
108        assert!(collective.owner_id.is_none());
109        assert!(collective.created_at == collective.updated_at);
110    }
111
112    #[test]
113    fn test_collective_with_owner() {
114        let collective = Collective::with_owner("test-project", "user-1", 768);
115        assert_eq!(collective.name, "test-project");
116        assert_eq!(collective.owner_id.as_deref(), Some("user-1"));
117        assert_eq!(collective.embedding_dimension, 768);
118    }
119
120    #[test]
121    fn test_collective_bincode_roundtrip() {
122        let collective = Collective::new("roundtrip-test", 384);
123        let bytes = bincode::serialize(&collective).unwrap();
124        let restored: Collective = bincode::deserialize(&bytes).unwrap();
125
126        assert_eq!(collective.id, restored.id);
127        assert_eq!(collective.name, restored.name);
128        assert_eq!(collective.owner_id, restored.owner_id);
129        assert_eq!(collective.embedding_dimension, restored.embedding_dimension);
130        assert_eq!(collective.created_at, restored.created_at);
131        assert_eq!(collective.updated_at, restored.updated_at);
132    }
133
134    #[test]
135    fn test_collective_bincode_roundtrip_with_owner() {
136        let collective = Collective::with_owner("owned-project", "tenant-42", 768);
137        let bytes = bincode::serialize(&collective).unwrap();
138        let restored: Collective = bincode::deserialize(&bytes).unwrap();
139
140        assert_eq!(collective.id, restored.id);
141        assert_eq!(collective.owner_id, restored.owner_id);
142    }
143}