Skip to main content

crawlkit_engine/
determinism.rs

1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::Arc;
5
6/// Determinism controller for reproducible crawl results.
7///
8/// Ensures same input + same config → same output.
9/// Uses seed-based PRNG for any randomized operations.
10///
11/// # Examples
12///
13/// ```rust
14/// use crawlkit_engine::DeterminismController;
15///
16/// let ctrl = DeterminismController::new(42);
17/// assert_eq!(ctrl.seed(), 42);
18/// let seed1 = ctrl.derive_seed("context1");
19/// let seed2 = ctrl.derive_seed("context1");
20/// assert_ne!(seed1, seed2); // counter makes each call unique
21/// ```
22pub struct DeterminismController {
23    /// Base seed for reproducible randomness.
24    seed: u64,
25    /// Whether determinism is enforced.
26    enforced: Arc<AtomicBool>,
27    /// Counter for unique seeding.
28    counter: AtomicU64,
29}
30
31impl DeterminismController {
32    /// Create a new determinism controller.
33    #[must_use]
34    pub fn new(seed: u64) -> Self {
35        Self {
36            seed,
37            enforced: Arc::new(AtomicBool::new(true)),
38            counter: AtomicU64::new(0),
39        }
40    }
41
42    /// Create with default seed (0).
43    #[must_use]
44    pub fn with_default_seed() -> Self {
45        Self::new(0)
46    }
47
48    /// Get the base seed.
49    #[must_use]
50    pub fn seed(&self) -> u64 {
51        self.seed
52    }
53
54    /// Generate a deterministic seed for a specific context.
55    #[must_use]
56    pub fn derive_seed(&self, context: &str) -> u64 {
57        let counter = self.counter.fetch_add(1, Ordering::AcqRel);
58        let mut hasher = DefaultHasher::new();
59        self.seed.hash(&mut hasher);
60        context.hash(&mut hasher);
61        counter.hash(&mut hasher);
62        hasher.finish()
63    }
64
65    /// Check if determinism is enforced.
66    #[must_use]
67    pub fn is_enforced(&self) -> bool {
68        self.enforced.load(Ordering::Acquire)
69    }
70
71    /// Enable or disable determinism enforcement.
72    pub fn set_enforced(&self, enforced: bool) {
73        self.enforced.store(enforced, Ordering::Release);
74    }
75
76    /// Compute deterministic hash for content.
77    #[must_use]
78    pub fn content_hash(content: &str) -> u64 {
79        let mut hasher = DefaultHasher::new();
80        content.hash(&mut hasher);
81        hasher.finish()
82    }
83
84    /// Compute deterministic hash for URL.
85    #[must_use]
86    pub fn url_hash(url: &str) -> u64 {
87        let mut hasher = DefaultHasher::new();
88        url.hash(&mut hasher);
89        hasher.finish()
90    }
91}
92
93impl Default for DeterminismController {
94    fn default() -> Self {
95        Self::with_default_seed()
96    }
97}
98
99/// Deterministic URL ordering for consistent output.
100pub fn deterministic_sort<T>(items: &mut [T], key_fn: impl Fn(&T) -> u64) {
101    items.sort_by_key(|a| key_fn(a));
102}
103
104// ---------------------------------------------------------------------------
105// Tests
106// ---------------------------------------------------------------------------
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_determinism_controller_seed() {
114        let ctrl = DeterminismController::new(42);
115        assert_eq!(ctrl.seed(), 42);
116        assert!(ctrl.is_enforced());
117    }
118
119    #[test]
120    fn test_determinism_controller_derive_seed() {
121        let ctrl = DeterminismController::new(42);
122        let seed1 = ctrl.derive_seed("context1");
123        let seed2 = ctrl.derive_seed("context2");
124        assert_ne!(seed1, seed2);
125    }
126
127    #[test]
128    fn test_determinism_controller_same_seed() {
129        let ctrl1 = DeterminismController::new(42);
130        let ctrl2 = DeterminismController::new(42);
131        assert_eq!(ctrl1.seed(), ctrl2.seed());
132    }
133
134    #[test]
135    fn test_content_hash_deterministic() {
136        let hash1 = DeterminismController::content_hash("hello");
137        let hash2 = DeterminismController::content_hash("hello");
138        assert_eq!(hash1, hash2);
139    }
140
141    #[test]
142    fn test_content_hash_different() {
143        let hash1 = DeterminismController::content_hash("hello");
144        let hash2 = DeterminismController::content_hash("world");
145        assert_ne!(hash1, hash2);
146    }
147}