1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct FuzzyMatchConfig {
8 pub threshold: f64,
10 pub levenshtein_weight: f64,
12 pub jaro_winkler_weight: f64,
14 pub use_aliases: bool,
16 pub use_ecosystem_rules: bool,
18 #[serde(default)]
20 pub field_weights: Option<MultiFieldWeights>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MultiFieldWeights {
32 pub name: f64,
34 pub version: f64,
36 pub ecosystem: f64,
38 pub licenses: f64,
40 pub supplier: f64,
42 pub group: f64,
44
45 #[serde(default)]
48 pub ecosystem_mismatch_penalty: f64,
49 #[serde(default = "default_true")]
51 pub version_divergence_enabled: bool,
52 #[serde(default = "default_version_major_penalty")]
54 pub version_major_penalty: f64,
55 #[serde(default = "default_version_minor_penalty")]
57 pub version_minor_penalty: f64,
58}
59
60const fn default_true() -> bool {
61 true
62}
63
64const fn default_version_major_penalty() -> f64 {
65 0.10
66}
67
68const fn default_version_minor_penalty() -> f64 {
69 0.02
70}
71
72impl MultiFieldWeights {
73 #[must_use]
75 pub const fn name_focused() -> Self {
76 Self {
77 name: 0.80,
78 version: 0.05,
79 ecosystem: 0.10,
80 licenses: 0.03,
81 supplier: 0.01,
82 group: 0.01,
83 ecosystem_mismatch_penalty: -0.15,
84 version_divergence_enabled: true,
85 version_major_penalty: 0.10,
86 version_minor_penalty: 0.02,
87 }
88 }
89
90 #[must_use]
92 pub const fn balanced() -> Self {
93 Self {
94 name: 0.60,
95 version: 0.10,
96 ecosystem: 0.15,
97 licenses: 0.08,
98 supplier: 0.04,
99 group: 0.03,
100 ecosystem_mismatch_penalty: -0.15, version_divergence_enabled: true,
102 version_major_penalty: 0.10,
103 version_minor_penalty: 0.02,
104 }
105 }
106
107 #[must_use]
109 pub const fn security_focused() -> Self {
110 Self {
111 name: 0.50,
112 version: 0.20,
113 ecosystem: 0.20,
114 licenses: 0.05,
115 supplier: 0.03,
116 group: 0.02,
117 ecosystem_mismatch_penalty: -0.25, version_divergence_enabled: true,
119 version_major_penalty: 0.15, version_minor_penalty: 0.03,
121 }
122 }
123
124 #[must_use]
129 pub const fn legacy() -> Self {
130 Self {
131 name: 0.60,
132 version: 0.10,
133 ecosystem: 0.15,
134 licenses: 0.08,
135 supplier: 0.04,
136 group: 0.03,
137 ecosystem_mismatch_penalty: 0.0, version_divergence_enabled: false, version_major_penalty: 0.0,
140 version_minor_penalty: 0.0,
141 }
142 }
143
144 #[must_use]
147 pub fn is_normalized(&self) -> bool {
148 let sum =
149 self.name + self.version + self.ecosystem + self.licenses + self.supplier + self.group;
150 (sum - 1.0).abs() < 0.001
151 }
152
153 pub fn normalize(&mut self) {
156 let sum =
157 self.name + self.version + self.ecosystem + self.licenses + self.supplier + self.group;
158 if sum > 0.0 {
159 self.name /= sum;
160 self.version /= sum;
161 self.ecosystem /= sum;
162 self.licenses /= sum;
163 self.supplier /= sum;
164 self.group /= sum;
165 }
166 }
167}
168
169impl Default for MultiFieldWeights {
170 fn default() -> Self {
171 Self::balanced()
172 }
173}
174
175impl FuzzyMatchConfig {
176 #[must_use]
178 pub const fn strict() -> Self {
179 Self {
180 threshold: 0.95,
181 levenshtein_weight: 0.5,
182 jaro_winkler_weight: 0.5,
183 use_aliases: true,
184 use_ecosystem_rules: true,
185 field_weights: None, }
187 }
188
189 #[must_use]
191 pub const fn balanced() -> Self {
192 Self {
193 threshold: 0.85,
194 levenshtein_weight: 0.4,
195 jaro_winkler_weight: 0.6,
196 use_aliases: true,
197 use_ecosystem_rules: true,
198 field_weights: None, }
200 }
201
202 #[must_use]
204 pub const fn permissive() -> Self {
205 Self {
206 threshold: 0.70,
207 levenshtein_weight: 0.3,
208 jaro_winkler_weight: 0.7,
209 use_aliases: true,
210 use_ecosystem_rules: true,
211 field_weights: None, }
213 }
214
215 #[must_use]
217 pub const fn with_multi_field(mut self, weights: MultiFieldWeights) -> Self {
218 self.field_weights = Some(weights);
219 self
220 }
221
222 #[must_use]
224 pub const fn with_threshold(mut self, threshold: f64) -> Self {
225 self.threshold = threshold;
226 self
227 }
228
229 #[must_use]
231 pub const fn strict_multi_field() -> Self {
232 Self::strict().with_multi_field(MultiFieldWeights::security_focused())
233 }
234
235 #[must_use]
237 pub const fn balanced_multi_field() -> Self {
238 Self::balanced().with_multi_field(MultiFieldWeights::balanced())
239 }
240
241 #[must_use]
248 pub fn from_preset(name: &str) -> Option<Self> {
249 match name.to_lowercase().as_str() {
250 "strict" => Some(Self::strict()),
251 "balanced" => Some(Self::balanced()),
252 "permissive" => Some(Self::permissive()),
253 "strict-multi" | "strict_multi" => Some(Self::strict_multi_field()),
254 "balanced-multi" | "balanced_multi" => Some(Self::balanced_multi_field()),
255 "security-focused" | "security_focused" => Some(Self::security_focused()),
256 _ => None,
257 }
258 }
259
260 #[must_use]
263 pub fn security_focused() -> Self {
264 Self {
265 threshold: 0.85,
266 field_weights: Some(MultiFieldWeights::security_focused()),
267 ..Self::strict()
268 }
269 }
270}
271
272impl Default for FuzzyMatchConfig {
273 fn default() -> Self {
274 Self::balanced()
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct CrossEcosystemConfig {
285 pub enabled: bool,
287 pub min_score: f64,
289 pub score_penalty: f64,
291 pub max_candidates: usize,
293 pub verified_only: bool,
295}
296
297impl Default for CrossEcosystemConfig {
298 fn default() -> Self {
299 Self {
300 enabled: true,
301 min_score: 0.80,
302 score_penalty: 0.10,
303 max_candidates: 10,
304 verified_only: false,
305 }
306 }
307}
308
309impl CrossEcosystemConfig {
310 #[must_use]
312 pub fn disabled() -> Self {
313 Self {
314 enabled: false,
315 ..Default::default()
316 }
317 }
318
319 #[must_use]
321 pub const fn strict() -> Self {
322 Self {
323 enabled: true,
324 min_score: 0.90,
325 score_penalty: 0.15,
326 max_candidates: 5,
327 verified_only: true,
328 }
329 }
330
331 #[must_use]
333 pub const fn permissive() -> Self {
334 Self {
335 enabled: true,
336 min_score: 0.70,
337 score_penalty: 0.05,
338 max_candidates: 20,
339 verified_only: false,
340 }
341 }
342}