Skip to main content

weavatrix_clone/
config.rs

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