1pub mod adversarial;
19pub mod docs;
20pub mod fairness;
21pub mod robustness;
22pub mod security;
23pub mod velocity;
24
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::path::PathBuf;
28use std::time::Duration;
29
30#[derive(Debug, Clone)]
32pub struct QaChecklist {
33 pub model_path: PathBuf,
35 pub test_data: Option<PathBuf>,
37 pub protected_attrs: Vec<String>,
39 pub latency_sla: Duration,
41 pub memory_budget: usize,
43 pub max_turns: u32,
45}
46
47impl Default for QaChecklist {
48 fn default() -> Self {
49 Self {
50 model_path: PathBuf::new(),
51 test_data: None,
52 protected_attrs: Vec::new(),
53 latency_sla: Duration::from_millis(100),
54 memory_budget: 512 * 1024 * 1024, max_turns: 5,
56 }
57 }
58}
59
60impl QaChecklist {
61 #[must_use]
63 pub fn new(model_path: PathBuf) -> Self {
64 Self {
65 model_path,
66 ..Default::default()
67 }
68 }
69
70 #[must_use]
72 pub fn with_test_data(mut self, path: PathBuf) -> Self {
73 self.test_data = Some(path);
74 self
75 }
76
77 #[must_use]
79 pub fn with_protected_attrs(mut self, attrs: Vec<String>) -> Self {
80 self.protected_attrs = attrs;
81 self
82 }
83
84 #[must_use]
86 pub fn with_latency_sla(mut self, sla: Duration) -> Self {
87 self.latency_sla = sla;
88 self
89 }
90
91 #[must_use]
93 pub fn with_memory_budget(mut self, budget: usize) -> Self {
94 self.memory_budget = budget;
95 self
96 }
97
98 #[must_use]
100 pub const fn max_score() -> u8 {
101 100
102 }
103
104 #[must_use]
106 pub fn category_points() -> HashMap<QaCategory, u8> {
107 let mut points = HashMap::new();
108 points.insert(QaCategory::Robustness, 20);
109 points.insert(QaCategory::EdgeCases, 15);
110 points.insert(QaCategory::DistributionShift, 15);
111 points.insert(QaCategory::Fairness, 15);
112 points.insert(QaCategory::Privacy, 10);
113 points.insert(QaCategory::Latency, 10);
114 points.insert(QaCategory::Memory, 10);
115 points.insert(QaCategory::Reproducibility, 5);
116 points
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct QaReport {
123 pub model_id: String,
125 pub categories: HashMap<QaCategory, CategoryScore>,
127 pub total_score: u8,
129 pub passed: bool,
131 pub blockers: Vec<QaIssue>,
133 pub warnings: Vec<QaIssue>,
135}
136
137impl QaReport {
138 #[must_use]
140 pub fn new(model_id: String) -> Self {
141 Self {
142 model_id,
143 categories: HashMap::new(),
144 total_score: 0,
145 passed: false,
146 blockers: Vec::new(),
147 warnings: Vec::new(),
148 }
149 }
150
151 pub fn add_category(&mut self, category: QaCategory, score: CategoryScore) {
153 self.categories.insert(category, score);
154 self.recalculate_total();
155 }
156
157 pub fn add_blocker(&mut self, issue: QaIssue) {
159 self.blockers.push(issue);
160 self.passed = false;
161 }
162
163 pub fn add_warning(&mut self, issue: QaIssue) {
165 self.warnings.push(issue);
166 }
167
168 fn recalculate_total(&mut self) {
170 let earned: u16 = self
171 .categories
172 .values()
173 .map(|s| u16::from(s.points_earned))
174 .sum();
175 let possible: u16 = self
176 .categories
177 .values()
178 .map(|s| u16::from(s.points_possible))
179 .sum();
180
181 self.total_score = if possible > 0 {
182 ((earned * 100) / possible).min(100) as u8
183 } else {
184 0
185 };
186
187 self.passed = self.total_score >= 80 && self.blockers.is_empty();
189 }
190
191 #[must_use]
193 pub fn is_production_ready(&self) -> bool {
194 self.passed && self.total_score >= 90
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
200pub enum QaCategory {
201 Robustness,
203 EdgeCases,
205 DistributionShift,
207 Fairness,
209 Privacy,
211 Latency,
213 Memory,
215 Reproducibility,
217}
218
219impl QaCategory {
220 #[must_use]
222 pub fn all() -> Vec<Self> {
223 vec![
224 Self::Robustness,
225 Self::EdgeCases,
226 Self::DistributionShift,
227 Self::Fairness,
228 Self::Privacy,
229 Self::Latency,
230 Self::Memory,
231 Self::Reproducibility,
232 ]
233 }
234
235 #[must_use]
237 pub const fn name(&self) -> &'static str {
238 match self {
239 Self::Robustness => "Robustness",
240 Self::EdgeCases => "Edge Cases",
241 Self::DistributionShift => "Distribution Shift",
242 Self::Fairness => "Fairness",
243 Self::Privacy => "Privacy",
244 Self::Latency => "Latency",
245 Self::Memory => "Memory",
246 Self::Reproducibility => "Reproducibility",
247 }
248 }
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct CategoryScore {
254 pub points_earned: u8,
256 pub points_possible: u8,
258 pub tests_passed: u32,
260 pub tests_failed: u32,
262 pub details: Vec<TestResult>,
264}
265
266impl CategoryScore {
267 #[must_use]
269 pub fn new(points_possible: u8) -> Self {
270 Self {
271 points_earned: 0,
272 points_possible,
273 tests_passed: 0,
274 tests_failed: 0,
275 details: Vec::new(),
276 }
277 }
278
279 pub fn add_result(&mut self, result: TestResult) {
281 if result.passed {
282 self.tests_passed += 1;
283 } else {
284 self.tests_failed += 1;
285 }
286 self.details.push(result);
287 }
288
289 pub fn finalize(&mut self) {
291 let total = self.tests_passed + self.tests_failed;
292 if total > 0 {
293 let pass_rate = f64::from(self.tests_passed) / f64::from(total);
294 self.points_earned = (f64::from(self.points_possible) * pass_rate).round() as u8;
295 }
296 }
297
298 #[must_use]
300 pub fn pass_rate(&self) -> f64 {
301 let total = self.tests_passed + self.tests_failed;
302 if total > 0 {
303 f64::from(self.tests_passed) / f64::from(total) * 100.0
304 } else {
305 0.0
306 }
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct TestResult {
313 pub name: String,
315 pub passed: bool,
317 pub message: Option<String>,
319 pub duration: Duration,
321}
322
323impl TestResult {
324 #[must_use]
326 pub fn pass(name: impl Into<String>, duration: Duration) -> Self {
327 Self {
328 name: name.into(),
329 passed: true,
330 message: None,
331 duration,
332 }
333 }
334
335 #[must_use]
337 pub fn fail(name: impl Into<String>, message: impl Into<String>, duration: Duration) -> Self {
338 Self {
339 name: name.into(),
340 passed: false,
341 message: Some(message.into()),
342 duration,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct QaIssue {
350 pub category: QaCategory,
352 pub severity: Severity,
354 pub message: String,
356 pub remediation: String,
358}
359
360impl QaIssue {
361 #[must_use]
363 pub fn new(
364 category: QaCategory,
365 severity: Severity,
366 message: impl Into<String>,
367 remediation: impl Into<String>,
368 ) -> Self {
369 Self {
370 category,
371 severity,
372 message: message.into(),
373 remediation: remediation.into(),
374 }
375 }
376
377 #[must_use]
379 pub fn blocker(
380 category: QaCategory,
381 message: impl Into<String>,
382 remediation: impl Into<String>,
383 ) -> Self {
384 Self::new(category, Severity::Blocker, message, remediation)
385 }
386
387 #[must_use]
389 pub fn warning(
390 category: QaCategory,
391 message: impl Into<String>,
392 remediation: impl Into<String>,
393 ) -> Self {
394 Self::new(category, Severity::Warning, message, remediation)
395 }
396}
397
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
400pub enum Severity {
401 Blocker,
403 Critical,
405 Warning,
407 Info,
409}
410
411impl Severity {
412 #[must_use]
414 pub const fn is_blocking(&self) -> bool {
415 matches!(self, Self::Blocker)
416 }
417
418 #[must_use]
420 pub const fn requires_review(&self) -> bool {
421 matches!(self, Self::Blocker | Self::Critical)
422 }
423}
424
425#[derive(Debug, Clone)]
427pub enum JidokaStop {
428 InvalidHeader,
430 SignatureFailed,
432 ChecksumFailed,
434 WcetViolation,
436 FairnessViolation,
438 QualityGateFailed {
440 score: u8,
442 threshold: u8,
444 },
445}
446
447impl JidokaStop {
448 #[must_use]
450 pub const fn requires_human_review(&self) -> bool {
451 true
452 }
453
454 #[must_use]
456 pub fn description(&self) -> String {
457 match self {
458 Self::InvalidHeader => "Invalid file header".to_string(),
459 Self::SignatureFailed => "Signature verification failed".to_string(),
460 Self::ChecksumFailed => "Checksum mismatch - data corrupted".to_string(),
461 Self::WcetViolation => "WCET budget exceeded".to_string(),
462 Self::FairnessViolation => "Fairness threshold breached".to_string(),
463 Self::QualityGateFailed { score, threshold } => {
464 format!("Quality gate failed: {score}/100 < {threshold}/100")
465 }
466 }
467 }
468}
469
470#[cfg(test)]
471#[path = "qa_tests.rs"]
472mod tests;