1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CostModel {
11 pub component_added: u32,
13 pub component_removed: u32,
15 pub version_patch: u32,
17 pub version_minor: u32,
19 pub version_major: u32,
21 pub license_changed: u32,
23 pub supplier_changed: u32,
25 pub ml_model_changed: u32,
27 pub dataset_changed: u32,
29 pub ml_approach_changed: u32,
31 pub ml_architecture_changed: u32,
33 pub ml_task_changed: u32,
35 pub ml_quantization_changed: u32,
37 pub ml_model_card_changed: u32,
39 pub ml_training_dataset_added: u32,
41 pub ml_training_dataset_removed: u32,
43 pub dataset_type_changed: u32,
45 pub dataset_governance_changed: u32,
47 pub dataset_sensitivity_added: u32,
49 pub dataset_sensitivity_removed: u32,
51 pub vulnerability_introduced: u32,
53 pub vulnerability_resolved: i32,
55 pub dependency_added: u32,
57 pub dependency_removed: u32,
59 pub hash_mismatch: u32,
61 pub crypto_algorithm_changed: u32,
63 pub crypto_downgrade: u32,
65 pub crypto_key_rotated: u32,
67 pub crypto_cert_expiry_changed: u32,
69 pub crypto_protocol_changed: u32,
71 pub crypto_quantum_level_changed: u32,
73}
74
75impl Default for CostModel {
76 fn default() -> Self {
77 Self {
78 component_added: 10,
79 component_removed: 10,
80 version_patch: 2,
81 version_minor: 4,
82 version_major: 7,
83 license_changed: 6,
84 supplier_changed: 4,
85 ml_model_changed: 6,
86 dataset_changed: 5,
87 ml_approach_changed: 6,
88 ml_architecture_changed: 6,
89 ml_task_changed: 5,
90 ml_quantization_changed: 8,
91 ml_model_card_changed: 3,
92 ml_training_dataset_added: 5,
93 ml_training_dataset_removed: 12,
94 dataset_type_changed: 5,
95 dataset_governance_changed: 4,
96 dataset_sensitivity_added: 14,
97 dataset_sensitivity_removed: 6,
98 vulnerability_introduced: 15,
99 vulnerability_resolved: -3,
100 dependency_added: 5,
101 dependency_removed: 5,
102 hash_mismatch: 8,
103 crypto_algorithm_changed: 8,
104 crypto_downgrade: 20,
105 crypto_key_rotated: 3,
106 crypto_cert_expiry_changed: 5,
107 crypto_protocol_changed: 6,
108 crypto_quantum_level_changed: 10,
109 }
110 }
111}
112
113impl CostModel {
114 #[must_use]
116 pub fn security_focused() -> Self {
117 Self {
118 vulnerability_introduced: 25,
119 vulnerability_resolved: -5,
120 hash_mismatch: 15,
121 supplier_changed: 8,
122 crypto_downgrade: 30,
123 crypto_quantum_level_changed: 15,
124 crypto_algorithm_changed: 12,
125 dataset_sensitivity_added: 22,
129 ml_training_dataset_removed: 18,
130 ml_quantization_changed: 12,
131 ..Default::default()
132 }
133 }
134
135 #[must_use]
137 pub fn compliance_focused() -> Self {
138 Self {
139 license_changed: 12,
140 supplier_changed: 8,
141 ..Default::default()
142 }
143 }
144
145 #[must_use]
147 pub const fn version_change_cost(
148 &self,
149 old: &Option<semver::Version>,
150 new: &Option<semver::Version>,
151 ) -> u32 {
152 match (old, new) {
153 (Some(old_ver), Some(new_ver)) => {
154 if old_ver.major != new_ver.major {
155 self.version_major
156 } else if old_ver.minor != new_ver.minor {
157 self.version_minor
158 } else if old_ver.patch != new_ver.patch {
159 self.version_patch
160 } else {
161 0
162 }
163 }
164 (None, Some(_)) | (Some(_), None) => self.version_minor,
165 (None, None) => 0,
166 }
167 }
168
169 #[allow(clippy::too_many_arguments)]
171 #[must_use]
172 pub const fn calculate_semantic_score(
173 &self,
174 components_added: usize,
175 components_removed: usize,
176 version_changes: usize,
177 license_changes: usize,
178 vulns_introduced: usize,
179 vulns_resolved: usize,
180 deps_added: usize,
181 deps_removed: usize,
182 ) -> f64 {
183 let score = (components_added as u32 * self.component_added)
184 + (components_removed as u32 * self.component_removed)
185 + (version_changes as u32 * self.version_minor)
186 + (license_changes as u32 * self.license_changed)
187 + (vulns_introduced as u32 * self.vulnerability_introduced)
188 + (deps_added as u32 * self.dependency_added)
189 + (deps_removed as u32 * self.dependency_removed);
190
191 let reward = vulns_resolved as i32 * self.vulnerability_resolved;
192
193 (score as i32 + reward) as f64
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_default_cost_model() {
203 let model = CostModel::default();
204 assert_eq!(model.component_added, 10);
205 assert_eq!(model.component_removed, 10);
206 assert_eq!(model.version_patch, 2);
207 assert_eq!(model.version_minor, 4);
208 assert_eq!(model.version_major, 7);
209 assert_eq!(model.license_changed, 6);
210 assert_eq!(model.supplier_changed, 4);
211 assert_eq!(model.ml_model_changed, 6);
212 assert_eq!(model.dataset_changed, 5);
213 assert_eq!(model.ml_approach_changed, 6);
214 assert_eq!(model.ml_architecture_changed, 6);
215 assert_eq!(model.ml_task_changed, 5);
216 assert_eq!(model.ml_quantization_changed, 8);
217 assert_eq!(model.ml_model_card_changed, 3);
218 assert_eq!(model.ml_training_dataset_added, 5);
219 assert_eq!(model.ml_training_dataset_removed, 12);
220 assert_eq!(model.dataset_type_changed, 5);
221 assert_eq!(model.dataset_governance_changed, 4);
222 assert_eq!(model.dataset_sensitivity_added, 14);
223 assert_eq!(model.dataset_sensitivity_removed, 6);
224 assert_eq!(model.vulnerability_introduced, 15);
225 assert_eq!(model.vulnerability_resolved, -3);
226 assert_eq!(model.hash_mismatch, 8);
227 assert_eq!(model.crypto_algorithm_changed, 8);
228 assert_eq!(model.crypto_downgrade, 20);
229 assert_eq!(model.crypto_key_rotated, 3);
230 assert_eq!(model.crypto_cert_expiry_changed, 5);
231 assert_eq!(model.crypto_protocol_changed, 6);
232 assert_eq!(model.crypto_quantum_level_changed, 10);
233 }
234
235 #[test]
236 fn test_security_focused_model() {
237 let model = CostModel::security_focused();
238 let default = CostModel::default();
239 assert!(model.vulnerability_introduced > default.vulnerability_introduced);
240 assert!(model.hash_mismatch > default.hash_mismatch);
241 assert!(model.vulnerability_resolved < default.vulnerability_resolved);
242 assert!(model.crypto_downgrade > default.crypto_downgrade);
243 assert!(model.crypto_quantum_level_changed > default.crypto_quantum_level_changed);
244 assert!(model.dataset_sensitivity_added > default.dataset_sensitivity_added);
245 assert!(model.ml_training_dataset_removed > default.ml_training_dataset_removed);
246 assert!(model.ml_quantization_changed > default.ml_quantization_changed);
247 }
248
249 #[test]
250 fn test_compliance_focused_model() {
251 let model = CostModel::compliance_focused();
252 let default = CostModel::default();
253 assert!(model.license_changed > default.license_changed);
254 assert!(model.supplier_changed > default.supplier_changed);
255 }
256
257 fn ver(major: u64, minor: u64, patch: u64) -> semver::Version {
258 semver::Version::new(major, minor, patch)
259 }
260
261 #[test]
262 fn test_version_change_cost_major() {
263 let model = CostModel::default();
264 let old = Some(ver(1, 0, 0));
265 let new = Some(ver(2, 0, 0));
266 assert_eq!(model.version_change_cost(&old, &new), model.version_major);
267 }
268
269 #[test]
270 fn test_version_change_cost_minor() {
271 let model = CostModel::default();
272 let old = Some(ver(1, 0, 0));
273 let new = Some(ver(1, 1, 0));
274 assert_eq!(model.version_change_cost(&old, &new), model.version_minor);
275 }
276
277 #[test]
278 fn test_version_change_cost_patch() {
279 let model = CostModel::default();
280 let old = Some(ver(1, 0, 0));
281 let new = Some(ver(1, 0, 1));
282 assert_eq!(model.version_change_cost(&old, &new), model.version_patch);
283 }
284
285 #[test]
286 fn test_version_change_cost_same() {
287 let model = CostModel::default();
288 let old = Some(ver(1, 2, 3));
289 let new = Some(ver(1, 2, 3));
290 assert_eq!(model.version_change_cost(&old, &new), 0);
291 }
292
293 #[test]
294 fn test_version_change_cost_none_to_some() {
295 let model = CostModel::default();
296 let new = Some(ver(1, 0, 0));
297 assert_eq!(model.version_change_cost(&None, &new), model.version_minor);
298 }
299
300 #[test]
301 fn test_version_change_cost_some_to_none() {
302 let model = CostModel::default();
303 let old = Some(ver(1, 0, 0));
304 assert_eq!(model.version_change_cost(&old, &None), model.version_minor);
305 }
306
307 #[test]
308 fn test_version_change_cost_none_none() {
309 let model = CostModel::default();
310 assert_eq!(model.version_change_cost(&None, &None), 0);
311 }
312
313 #[test]
314 fn test_semantic_score_zero() {
315 let model = CostModel::default();
316 let score = model.calculate_semantic_score(0, 0, 0, 0, 0, 0, 0, 0);
317 assert_eq!(score, 0.0);
318 }
319
320 #[test]
321 fn test_semantic_score_basic() {
322 let model = CostModel::default();
323 let score = model.calculate_semantic_score(2, 1, 1, 0, 0, 0, 0, 0);
325 assert_eq!(score, 34.0);
326 }
327
328 #[test]
329 fn test_semantic_score_with_resolved() {
330 let model = CostModel::default();
331 let score = model.calculate_semantic_score(0, 0, 0, 0, 1, 2, 0, 0);
333 assert_eq!(score, 9.0);
334 }
335}