crawlkit-engine 1.0.0

High-performance Rust web crawler and SEO analysis toolkit with 28 analyzers, WASM plugin system, and enterprise features
Documentation
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;

/// Determinism controller for reproducible crawl results.
///
/// Ensures same input + same config → same output.
/// Uses seed-based PRNG for any randomized operations.
///
/// # Examples
///
/// ```rust
/// use crawlkit_engine::DeterminismController;
///
/// let ctrl = DeterminismController::new(42);
/// assert_eq!(ctrl.seed(), 42);
/// let seed1 = ctrl.derive_seed("context1");
/// let seed2 = ctrl.derive_seed("context1");
/// assert_ne!(seed1, seed2); // counter makes each call unique
/// ```
pub struct DeterminismController {
    /// Base seed for reproducible randomness.
    seed: u64,
    /// Whether determinism is enforced.
    enforced: Arc<AtomicBool>,
    /// Counter for unique seeding.
    counter: AtomicU64,
}

impl DeterminismController {
    /// Create a new determinism controller.
    #[must_use]
    pub fn new(seed: u64) -> Self {
        Self {
            seed,
            enforced: Arc::new(AtomicBool::new(true)),
            counter: AtomicU64::new(0),
        }
    }

    /// Create with default seed (0).
    #[must_use]
    pub fn with_default_seed() -> Self {
        Self::new(0)
    }

    /// Get the base seed.
    #[must_use]
    pub fn seed(&self) -> u64 {
        self.seed
    }

    /// Generate a deterministic seed for a specific context.
    #[must_use]
    pub fn derive_seed(&self, context: &str) -> u64 {
        let counter = self.counter.fetch_add(1, Ordering::AcqRel);
        let mut hasher = DefaultHasher::new();
        self.seed.hash(&mut hasher);
        context.hash(&mut hasher);
        counter.hash(&mut hasher);
        hasher.finish()
    }

    /// Check if determinism is enforced.
    #[must_use]
    pub fn is_enforced(&self) -> bool {
        self.enforced.load(Ordering::Acquire)
    }

    /// Enable or disable determinism enforcement.
    pub fn set_enforced(&self, enforced: bool) {
        self.enforced.store(enforced, Ordering::Release);
    }

    /// Compute deterministic hash for content.
    #[must_use]
    pub fn content_hash(content: &str) -> u64 {
        let mut hasher = DefaultHasher::new();
        content.hash(&mut hasher);
        hasher.finish()
    }

    /// Compute deterministic hash for URL.
    #[must_use]
    pub fn url_hash(url: &str) -> u64 {
        let mut hasher = DefaultHasher::new();
        url.hash(&mut hasher);
        hasher.finish()
    }
}

impl Default for DeterminismController {
    fn default() -> Self {
        Self::with_default_seed()
    }
}

/// Deterministic URL ordering for consistent output.
pub fn deterministic_sort<T>(items: &mut [T], key_fn: impl Fn(&T) -> u64) {
    items.sort_by_key(|a| key_fn(a));
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_determinism_controller_seed() {
        let ctrl = DeterminismController::new(42);
        assert_eq!(ctrl.seed(), 42);
        assert!(ctrl.is_enforced());
    }

    #[test]
    fn test_determinism_controller_derive_seed() {
        let ctrl = DeterminismController::new(42);
        let seed1 = ctrl.derive_seed("context1");
        let seed2 = ctrl.derive_seed("context2");
        assert_ne!(seed1, seed2);
    }

    #[test]
    fn test_determinism_controller_same_seed() {
        let ctrl1 = DeterminismController::new(42);
        let ctrl2 = DeterminismController::new(42);
        assert_eq!(ctrl1.seed(), ctrl2.seed());
    }

    #[test]
    fn test_content_hash_deterministic() {
        let hash1 = DeterminismController::content_hash("hello");
        let hash2 = DeterminismController::content_hash("hello");
        assert_eq!(hash1, hash2);
    }

    #[test]
    fn test_content_hash_different() {
        let hash1 = DeterminismController::content_hash("hello");
        let hash2 = DeterminismController::content_hash("world");
        assert_ne!(hash1, hash2);
    }
}