Skip to main content

weavatrix_clone/
config.rs

1use crate::{CloneError, DetectionMode, Result, Similarity};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct CloneConfig {
5    pub mode: DetectionMode,
6    pub min_tokens: usize,
7    pub k_gram: usize,
8    pub winnowing_window: usize,
9    pub min_similarity: Similarity,
10    pub candidate_similarity: Similarity,
11    pub min_shared_fingerprints: usize,
12    pub max_bucket_size: usize,
13    pub max_fragments: usize,
14    pub max_tokens_per_fragment: usize,
15    pub max_candidates: usize,
16    pub compare_overlapping_fragments: bool,
17}
18
19impl Default for CloneConfig {
20    fn default() -> Self {
21        Self {
22            mode: DetectionMode::NearMiss,
23            min_tokens: 24,
24            k_gram: 8,
25            winnowing_window: 4,
26            min_similarity: Similarity::from_permille(800),
27            candidate_similarity: Similarity::from_permille(450),
28            min_shared_fingerprints: 2,
29            max_bucket_size: 128,
30            max_fragments: 1_000_000,
31            max_tokens_per_fragment: 100_000,
32            max_candidates: 5_000_000,
33            compare_overlapping_fragments: false,
34        }
35    }
36}
37
38impl CloneConfig {
39    /// Validates safety bounds and detection thresholds.
40    ///
41    /// # Errors
42    ///
43    /// Rejects zero bounds, impossible winnowing sizes, and a candidate
44    /// threshold above the final verification threshold.
45    pub fn validate(self) -> Result<Self> {
46        if self.k_gram == 0 {
47            return Err(invalid("k_gram", "must be greater than zero"));
48        }
49        if self.winnowing_window == 0 {
50            return Err(invalid("winnowing_window", "must be greater than zero"));
51        }
52        let guaranteed = self
53            .k_gram
54            .checked_add(self.winnowing_window)
55            .and_then(|value| value.checked_sub(1))
56            .ok_or(CloneError::CapacityExceeded {
57                resource: "winnowing guarantee",
58                limit: usize::MAX,
59            })?;
60        if self.min_tokens < guaranteed {
61            return Err(invalid(
62                "min_tokens",
63                "must cover at least one complete winnowing window",
64            ));
65        }
66        if self.candidate_similarity > self.min_similarity {
67            return Err(invalid(
68                "candidate_similarity",
69                "must not exceed min_similarity",
70            ));
71        }
72        for (field, value) in [
73            ("min_shared_fingerprints", self.min_shared_fingerprints),
74            ("max_bucket_size", self.max_bucket_size),
75            ("max_fragments", self.max_fragments),
76            ("max_tokens_per_fragment", self.max_tokens_per_fragment),
77            ("max_candidates", self.max_candidates),
78        ] {
79            if value == 0 {
80                return Err(invalid(field, "must be greater than zero"));
81            }
82        }
83        Ok(self)
84    }
85}
86
87const fn invalid(field: &'static str, reason: &'static str) -> CloneError {
88    CloneError::InvalidConfig { field, reason }
89}