1#![allow(
2 clippy::derive_partial_eq_without_eq,
3 clippy::doc_markdown,
4 clippy::missing_const_for_fn
5)]
6use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
13pub enum Grade {
14 #[default]
16 F,
17 D,
19 CMinus,
21 C,
23 CPlus,
25 BMinus,
27 B,
29 BPlus,
31 AMinus,
33 A,
35 APlus,
37}
38
39impl PartialOrd for Grade {
40 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
41 Some(self.cmp(other))
42 }
43}
44
45impl Ord for Grade {
46 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
47 self.min_percentage()
49 .partial_cmp(&other.min_percentage())
50 .unwrap_or(std::cmp::Ordering::Equal)
51 }
52}
53
54impl Grade {
55 #[must_use]
57 pub fn from_percentage(percent: f32) -> Self {
58 match percent {
59 p if p >= 95.0 => Self::APlus,
60 p if p >= 90.0 => Self::A,
61 p if p >= 85.0 => Self::AMinus,
62 p if p >= 80.0 => Self::BPlus,
63 p if p >= 75.0 => Self::B,
64 p if p >= 70.0 => Self::BMinus,
65 p if p >= 65.0 => Self::CPlus,
66 p if p >= 60.0 => Self::C,
67 p if p >= 55.0 => Self::CMinus,
68 p if p >= 50.0 => Self::D,
69 _ => Self::F,
70 }
71 }
72
73 #[must_use]
75 pub const fn min_percentage(&self) -> f32 {
76 match self {
77 Self::APlus => 95.0,
78 Self::A => 90.0,
79 Self::AMinus => 85.0,
80 Self::BPlus => 80.0,
81 Self::B => 75.0,
82 Self::BMinus => 70.0,
83 Self::CPlus => 65.0,
84 Self::C => 60.0,
85 Self::CMinus => 55.0,
86 Self::D => 50.0,
87 Self::F => 0.0,
88 }
89 }
90
91 #[must_use]
93 pub const fn letter(&self) -> &'static str {
94 match self {
95 Self::APlus => "A+",
96 Self::A => "A",
97 Self::AMinus => "A-",
98 Self::BPlus => "B+",
99 Self::B => "B",
100 Self::BMinus => "B-",
101 Self::CPlus => "C+",
102 Self::C => "C",
103 Self::CMinus => "C-",
104 Self::D => "D",
105 Self::F => "F",
106 }
107 }
108
109 #[must_use]
111 pub const fn is_passing(&self) -> bool {
112 matches!(
113 self,
114 Self::APlus
115 | Self::A
116 | Self::AMinus
117 | Self::BPlus
118 | Self::B
119 | Self::BMinus
120 | Self::CPlus
121 | Self::C
122 )
123 }
124
125 #[must_use]
127 pub const fn is_production_ready(&self) -> bool {
128 matches!(self, Self::APlus | Self::A | Self::AMinus | Self::BPlus)
129 }
130}
131
132impl std::fmt::Display for Grade {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}", self.letter())
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Criterion {
141 pub name: String,
143 pub description: String,
145 pub weight: f32,
147 pub score: f32,
149 pub passed: bool,
151 pub feedback: Option<String>,
153}
154
155impl Criterion {
156 #[must_use]
158 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
159 Self {
160 name: name.into(),
161 description: description.into(),
162 weight: 1.0,
163 score: 0.0,
164 passed: false,
165 feedback: None,
166 }
167 }
168
169 #[must_use]
171 pub fn weight(mut self, weight: f32) -> Self {
172 self.weight = weight.clamp(0.0, 1.0);
173 self
174 }
175
176 #[must_use]
178 pub fn score(mut self, score: f32) -> Self {
179 self.score = score.clamp(0.0, 100.0);
180 self.passed = self.score >= 60.0;
181 self
182 }
183
184 #[must_use]
186 pub const fn pass(mut self) -> Self {
187 self.score = 100.0;
188 self.passed = true;
189 self
190 }
191
192 #[must_use]
194 pub const fn fail(mut self) -> Self {
195 self.score = 0.0;
196 self.passed = false;
197 self
198 }
199
200 #[must_use]
202 pub fn feedback(mut self, feedback: impl Into<String>) -> Self {
203 self.feedback = Some(feedback.into());
204 self
205 }
206
207 #[must_use]
209 pub fn grade(&self) -> Grade {
210 Grade::from_percentage(self.score)
211 }
212
213 #[must_use]
215 pub fn weighted_score(&self) -> f32 {
216 self.score * self.weight
217 }
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
222pub struct ReportCard {
223 pub title: String,
225 pub criteria: Vec<Criterion>,
227 pub categories: HashMap<String, f32>,
229}
230
231impl ReportCard {
232 #[must_use]
234 pub fn new(title: impl Into<String>) -> Self {
235 Self {
236 title: title.into(),
237 criteria: Vec::new(),
238 categories: HashMap::new(),
239 }
240 }
241
242 pub fn add_criterion(&mut self, criterion: Criterion) {
244 self.criteria.push(criterion);
245 }
246
247 #[must_use]
249 pub fn criterion(mut self, criterion: Criterion) -> Self {
250 self.criteria.push(criterion);
251 self
252 }
253
254 pub fn add_category(&mut self, name: impl Into<String>, score: f32) {
256 self.categories.insert(name.into(), score.clamp(0.0, 100.0));
257 }
258
259 #[must_use]
261 pub fn overall_score(&self) -> f32 {
262 if self.criteria.is_empty() {
263 return 0.0;
264 }
265
266 let total_weight: f32 = self.criteria.iter().map(|c| c.weight).sum();
267 if total_weight == 0.0 {
268 return 0.0;
269 }
270
271 let weighted_sum: f32 = self.criteria.iter().map(Criterion::weighted_score).sum();
272 weighted_sum / total_weight
273 }
274
275 #[must_use]
277 pub fn overall_grade(&self) -> Grade {
278 Grade::from_percentage(self.overall_score())
279 }
280
281 #[must_use]
283 pub fn all_passed(&self) -> bool {
284 self.criteria.iter().all(|c| c.passed)
285 }
286
287 #[must_use]
289 pub fn passed_count(&self) -> usize {
290 self.criteria.iter().filter(|c| c.passed).count()
291 }
292
293 #[must_use]
295 pub fn failed_count(&self) -> usize {
296 self.criteria.iter().filter(|c| !c.passed).count()
297 }
298
299 #[must_use]
301 pub fn failures(&self) -> Vec<&Criterion> {
302 self.criteria.iter().filter(|c| !c.passed).collect()
303 }
304
305 #[must_use]
307 pub fn is_passing(&self) -> bool {
308 self.overall_grade().is_passing()
309 }
310}
311
312pub mod categories {
314 pub const ACCESSIBILITY: &str = "accessibility";
316 pub const PERFORMANCE: &str = "performance";
318 pub const VISUAL: &str = "visual";
320 pub const CODE_QUALITY: &str = "code_quality";
322 pub const TESTING: &str = "testing";
324 pub const DOCUMENTATION: &str = "documentation";
326 pub const SECURITY: &str = "security";
328}
329
330#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
336pub struct ScoreBreakdown {
337 pub widget_complexity: f64,
340 pub layout_depth: f64,
342 pub component_count: f64,
344
345 pub render_time_p95: f64,
348 pub memory_usage: f64,
350 pub bundle_size: f64,
352
353 pub wcag_aa_compliance: f64,
356 pub keyboard_navigation: f64,
358 pub screen_reader: f64,
360
361 pub data_completeness: f64,
364 pub data_freshness: f64,
366 pub schema_validation: f64,
368
369 pub manifest_completeness: f64,
372 pub card_coverage: f64,
374
375 pub theme_adherence: f64,
378 pub naming_conventions: f64,
380}
381
382impl ScoreBreakdown {
383 #[must_use]
385 pub fn structural_score(&self) -> f64 {
386 (self.widget_complexity + self.layout_depth + self.component_count) / 3.0 * 0.25
387 }
388
389 #[must_use]
391 pub fn performance_score(&self) -> f64 {
392 (self.render_time_p95 + self.memory_usage + self.bundle_size) / 3.0 * 0.20
393 }
394
395 #[must_use]
397 pub fn accessibility_score(&self) -> f64 {
398 (self.wcag_aa_compliance + self.keyboard_navigation + self.screen_reader) / 3.0 * 0.20
399 }
400
401 #[must_use]
403 pub fn data_quality_score(&self) -> f64 {
404 (self.data_completeness + self.data_freshness + self.schema_validation) / 3.0 * 0.15
405 }
406
407 #[must_use]
409 pub fn documentation_score(&self) -> f64 {
410 (self.manifest_completeness + self.card_coverage) / 2.0 * 0.10
411 }
412
413 #[must_use]
415 pub fn consistency_score(&self) -> f64 {
416 (self.theme_adherence + self.naming_conventions) / 2.0 * 0.10
417 }
418
419 #[must_use]
421 pub fn total(&self) -> f64 {
422 self.structural_score()
423 + self.performance_score()
424 + self.accessibility_score()
425 + self.data_quality_score()
426 + self.documentation_score()
427 + self.consistency_score()
428 }
429}
430
431#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
433pub struct AppQualityScore {
434 pub overall: f64,
436 pub grade: Grade,
438 pub breakdown: ScoreBreakdown,
440}
441
442impl AppQualityScore {
443 #[must_use]
445 pub fn from_breakdown(breakdown: ScoreBreakdown) -> Self {
446 let overall = breakdown.total();
447 let grade = Grade::from_percentage(overall as f32);
448 Self {
449 overall,
450 grade,
451 breakdown,
452 }
453 }
454
455 #[must_use]
457 pub fn meets_minimum(&self, min_grade: Grade) -> bool {
458 self.grade >= min_grade
459 }
460
461 #[must_use]
463 pub fn is_production_ready(&self) -> bool {
464 self.grade.is_production_ready()
465 }
466}
467
468#[derive(Debug, Clone, Default)]
470pub struct QualityScoreBuilder {
471 breakdown: ScoreBreakdown,
472}
473
474impl QualityScoreBuilder {
475 #[must_use]
477 pub fn new() -> Self {
478 Self::default()
479 }
480
481 #[must_use]
483 pub fn structural(mut self, complexity: f64, depth: f64, count: f64) -> Self {
484 self.breakdown.widget_complexity = complexity.clamp(0.0, 100.0);
485 self.breakdown.layout_depth = depth.clamp(0.0, 100.0);
486 self.breakdown.component_count = count.clamp(0.0, 100.0);
487 self
488 }
489
490 #[must_use]
492 pub fn performance(mut self, render_time: f64, memory: f64, bundle: f64) -> Self {
493 self.breakdown.render_time_p95 = render_time.clamp(0.0, 100.0);
494 self.breakdown.memory_usage = memory.clamp(0.0, 100.0);
495 self.breakdown.bundle_size = bundle.clamp(0.0, 100.0);
496 self
497 }
498
499 #[must_use]
501 pub fn accessibility(mut self, wcag: f64, keyboard: f64, screen_reader: f64) -> Self {
502 self.breakdown.wcag_aa_compliance = wcag.clamp(0.0, 100.0);
503 self.breakdown.keyboard_navigation = keyboard.clamp(0.0, 100.0);
504 self.breakdown.screen_reader = screen_reader.clamp(0.0, 100.0);
505 self
506 }
507
508 #[must_use]
510 pub fn data_quality(mut self, completeness: f64, freshness: f64, schema: f64) -> Self {
511 self.breakdown.data_completeness = completeness.clamp(0.0, 100.0);
512 self.breakdown.data_freshness = freshness.clamp(0.0, 100.0);
513 self.breakdown.schema_validation = schema.clamp(0.0, 100.0);
514 self
515 }
516
517 #[must_use]
519 pub fn documentation(mut self, manifest: f64, cards: f64) -> Self {
520 self.breakdown.manifest_completeness = manifest.clamp(0.0, 100.0);
521 self.breakdown.card_coverage = cards.clamp(0.0, 100.0);
522 self
523 }
524
525 #[must_use]
527 pub fn consistency(mut self, theme: f64, naming: f64) -> Self {
528 self.breakdown.theme_adherence = theme.clamp(0.0, 100.0);
529 self.breakdown.naming_conventions = naming.clamp(0.0, 100.0);
530 self
531 }
532
533 #[must_use]
535 pub fn build(self) -> AppQualityScore {
536 AppQualityScore::from_breakdown(self.breakdown)
537 }
538}
539
540#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
546pub struct QualityGates {
547 pub min_grade: Grade,
549 pub min_score: f64,
551 pub performance: PerformanceGates,
553 pub accessibility: AccessibilityGates,
555 pub data: DataGates,
557 pub documentation: DocumentationGates,
559}
560
561#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563pub struct PerformanceGates {
564 pub max_render_time_ms: u32,
566 pub max_bundle_size_kb: u32,
568 pub max_memory_mb: u32,
570}
571
572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
574pub struct AccessibilityGates {
575 pub wcag_level: String,
577 pub min_contrast_ratio: f32,
579 pub require_keyboard_nav: bool,
581 pub require_aria_labels: bool,
583}
584
585#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct DataGates {
588 pub max_staleness_minutes: u32,
590 pub require_schema_validation: bool,
592}
593
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596pub struct DocumentationGates {
597 pub require_model_cards: bool,
599 pub require_data_cards: bool,
601 pub min_manifest_fields: Vec<String>,
603}
604
605impl Default for QualityGates {
606 fn default() -> Self {
607 Self {
608 min_grade: Grade::BPlus,
609 min_score: 80.0,
610 performance: PerformanceGates::default(),
611 accessibility: AccessibilityGates::default(),
612 data: DataGates::default(),
613 documentation: DocumentationGates::default(),
614 }
615 }
616}
617
618impl Default for PerformanceGates {
619 fn default() -> Self {
620 Self {
621 max_render_time_ms: 16,
622 max_bundle_size_kb: 500,
623 max_memory_mb: 100,
624 }
625 }
626}
627
628impl Default for AccessibilityGates {
629 fn default() -> Self {
630 Self {
631 wcag_level: "AA".to_string(),
632 min_contrast_ratio: 4.5,
633 require_keyboard_nav: true,
634 require_aria_labels: true,
635 }
636 }
637}
638
639impl Default for DataGates {
640 fn default() -> Self {
641 Self {
642 max_staleness_minutes: 60,
643 require_schema_validation: true,
644 }
645 }
646}
647
648impl Default for DocumentationGates {
649 fn default() -> Self {
650 Self {
651 require_model_cards: true,
652 require_data_cards: true,
653 min_manifest_fields: vec![
654 "name".to_string(),
655 "version".to_string(),
656 "description".to_string(),
657 ],
658 }
659 }
660}
661
662#[derive(Debug, Clone)]
664pub struct GateCheckResult {
665 pub passed: bool,
667 pub violations: Vec<GateViolation>,
669}
670
671#[derive(Debug, Clone)]
673pub struct GateViolation {
674 pub gate: String,
676 pub expected: String,
678 pub actual: String,
680 pub severity: ViolationSeverity,
682}
683
684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum ViolationSeverity {
687 Warning,
689 Error,
691}
692
693impl QualityGates {
694 #[must_use]
696 pub fn check(&self, score: &AppQualityScore) -> GateCheckResult {
697 let mut violations = Vec::new();
698
699 if score.grade < self.min_grade {
701 violations.push(GateViolation {
702 gate: "min_grade".to_string(),
703 expected: self.min_grade.letter().to_string(),
704 actual: score.grade.letter().to_string(),
705 severity: ViolationSeverity::Error,
706 });
707 }
708
709 if score.overall < self.min_score {
711 violations.push(GateViolation {
712 gate: "min_score".to_string(),
713 expected: format!("{:.1}", self.min_score),
714 actual: format!("{:.1}", score.overall),
715 severity: ViolationSeverity::Error,
716 });
717 }
718
719 GateCheckResult {
720 passed: violations.is_empty(),
721 violations,
722 }
723 }
724}
725
726#[derive(Debug, Clone, Default)]
728pub struct EvaluationBuilder {
729 report: ReportCard,
730}
731
732impl EvaluationBuilder {
733 #[must_use]
735 pub fn new(title: impl Into<String>) -> Self {
736 Self {
737 report: ReportCard::new(title),
738 }
739 }
740
741 #[must_use]
743 pub fn accessibility(mut self, score: f32, feedback: Option<&str>) -> Self {
744 let mut criterion = Criterion::new(
745 "Accessibility",
746 "WCAG 2.1 AA compliance and screen reader support",
747 )
748 .weight(1.0)
749 .score(score);
750
751 if let Some(fb) = feedback {
752 criterion = criterion.feedback(fb);
753 }
754
755 self.report.add_criterion(criterion);
756 self.report
757 .add_category(categories::ACCESSIBILITY.to_string(), score);
758 self
759 }
760
761 #[must_use]
763 pub fn performance(mut self, score: f32, feedback: Option<&str>) -> Self {
764 let mut criterion = Criterion::new(
765 "Performance",
766 "Frame rate, memory usage, and responsiveness",
767 )
768 .weight(1.0)
769 .score(score);
770
771 if let Some(fb) = feedback {
772 criterion = criterion.feedback(fb);
773 }
774
775 self.report.add_criterion(criterion);
776 self.report
777 .add_category(categories::PERFORMANCE.to_string(), score);
778 self
779 }
780
781 #[must_use]
783 pub fn visual(mut self, score: f32, feedback: Option<&str>) -> Self {
784 let mut criterion =
785 Criterion::new("Visual Consistency", "Theme adherence and visual polish")
786 .weight(0.8)
787 .score(score);
788
789 if let Some(fb) = feedback {
790 criterion = criterion.feedback(fb);
791 }
792
793 self.report.add_criterion(criterion);
794 self.report
795 .add_category(categories::VISUAL.to_string(), score);
796 self
797 }
798
799 #[must_use]
801 pub fn code_quality(mut self, score: f32, feedback: Option<&str>) -> Self {
802 let mut criterion = Criterion::new(
803 "Code Quality",
804 "Lint compliance, documentation, and maintainability",
805 )
806 .weight(0.8)
807 .score(score);
808
809 if let Some(fb) = feedback {
810 criterion = criterion.feedback(fb);
811 }
812
813 self.report.add_criterion(criterion);
814 self.report
815 .add_category(categories::CODE_QUALITY.to_string(), score);
816 self
817 }
818
819 #[must_use]
821 pub fn testing(mut self, score: f32, feedback: Option<&str>) -> Self {
822 let mut criterion = Criterion::new("Testing", "Test coverage and mutation testing score")
823 .weight(1.0)
824 .score(score);
825
826 if let Some(fb) = feedback {
827 criterion = criterion.feedback(fb);
828 }
829
830 self.report.add_criterion(criterion);
831 self.report
832 .add_category(categories::TESTING.to_string(), score);
833 self
834 }
835
836 #[must_use]
838 pub fn custom(mut self, criterion: Criterion) -> Self {
839 self.report.add_criterion(criterion);
840 self
841 }
842
843 #[must_use]
845 pub fn build(self) -> ReportCard {
846 self.report
847 }
848}
849
850#[derive(Debug, Clone, PartialEq)]
856pub enum GateConfigError {
857 ParseError(String),
859 IoError(String),
861 InvalidValue(String),
863}
864
865impl std::fmt::Display for GateConfigError {
866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867 match self {
868 Self::ParseError(msg) => write!(f, "parse error: {msg}"),
869 Self::IoError(msg) => write!(f, "IO error: {msg}"),
870 Self::InvalidValue(msg) => write!(f, "invalid value: {msg}"),
871 }
872 }
873}
874
875impl std::error::Error for GateConfigError {}
876
877impl QualityGates {
878 pub const CONFIG_FILE: &'static str = ".presentar-gates.toml";
880
881 pub fn from_toml(toml_str: &str) -> Result<Self, GateConfigError> {
887 toml::from_str(toml_str).map_err(|e| GateConfigError::ParseError(e.to_string()))
888 }
889
890 #[must_use]
892 pub fn to_toml(&self) -> String {
893 toml::to_string_pretty(self).unwrap_or_default()
894 }
895
896 pub fn load_from_file(path: &std::path::Path) -> Result<Self, GateConfigError> {
902 let contents =
903 std::fs::read_to_string(path).map_err(|e| GateConfigError::IoError(e.to_string()))?;
904 Self::from_toml(&contents)
905 }
906
907 pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), GateConfigError> {
913 let contents = self.to_toml();
914 std::fs::write(path, contents).map_err(|e| GateConfigError::IoError(e.to_string()))
915 }
916
917 #[must_use]
921 pub fn load_default() -> Self {
922 let path = std::path::Path::new(Self::CONFIG_FILE);
923 Self::load_from_file(path).unwrap_or_default()
924 }
925
926 #[must_use]
928 pub fn check_extended(
929 &self,
930 score: &AppQualityScore,
931 render_time_ms: Option<u32>,
932 bundle_size_kb: Option<u32>,
933 memory_mb: Option<u32>,
934 ) -> GateCheckResult {
935 let mut result = self.check(score);
936
937 if let Some(render_time) = render_time_ms {
939 if render_time > self.performance.max_render_time_ms {
940 result.violations.push(GateViolation {
941 gate: "max_render_time_ms".to_string(),
942 expected: format!("<= {}ms", self.performance.max_render_time_ms),
943 actual: format!("{}ms", render_time),
944 severity: ViolationSeverity::Error,
945 });
946 }
947 }
948
949 if let Some(bundle) = bundle_size_kb {
950 if bundle > self.performance.max_bundle_size_kb {
951 result.violations.push(GateViolation {
952 gate: "max_bundle_size_kb".to_string(),
953 expected: format!("<= {}KB", self.performance.max_bundle_size_kb),
954 actual: format!("{}KB", bundle),
955 severity: ViolationSeverity::Error,
956 });
957 }
958 }
959
960 if let Some(memory) = memory_mb {
961 if memory > self.performance.max_memory_mb {
962 result.violations.push(GateViolation {
963 gate: "max_memory_mb".to_string(),
964 expected: format!("<= {}MB", self.performance.max_memory_mb),
965 actual: format!("{}MB", memory),
966 severity: ViolationSeverity::Warning,
967 });
968 }
969 }
970
971 result.passed = result
972 .violations
973 .iter()
974 .all(|v| v.severity != ViolationSeverity::Error);
975 result
976 }
977
978 #[must_use]
980 pub fn sample_config() -> String {
981 r#"# Presentar Quality Gates Configuration
982# Place this file at .presentar-gates.toml in your project root
983
984# Minimum required grade (F, D, C, C+, B-, B, B+, A-, A, A+)
985min_grade = "B+"
986
987# Minimum required score (0-100)
988min_score = 80.0
989
990[performance]
991# Maximum render time in milliseconds (60fps = 16ms)
992max_render_time_ms = 16
993
994# Maximum bundle size in KB
995max_bundle_size_kb = 500
996
997# Maximum memory usage in MB
998max_memory_mb = 100
999
1000[accessibility]
1001# WCAG level: "A", "AA", or "AAA"
1002wcag_level = "AA"
1003
1004# Minimum contrast ratio
1005min_contrast_ratio = 4.5
1006
1007# Require full keyboard navigation
1008require_keyboard_nav = true
1009
1010# Require ARIA labels
1011require_aria_labels = true
1012
1013[data]
1014# Maximum data staleness in minutes
1015max_staleness_minutes = 60
1016
1017# Require schema validation
1018require_schema_validation = true
1019
1020[documentation]
1021# Require model cards for ML models
1022require_model_cards = true
1023
1024# Require data cards for datasets
1025require_data_cards = true
1026
1027# Minimum required manifest fields
1028min_manifest_fields = ["name", "version", "description"]
1029"#
1030 .to_string()
1031 }
1032}
1033
1034impl Grade {
1035 pub fn from_str(s: &str) -> Result<Self, GateConfigError> {
1041 match s.trim().to_uppercase().as_str() {
1042 "A+" => Ok(Self::APlus),
1043 "A" => Ok(Self::A),
1044 "A-" => Ok(Self::AMinus),
1045 "B+" => Ok(Self::BPlus),
1046 "B" => Ok(Self::B),
1047 "B-" => Ok(Self::BMinus),
1048 "C+" => Ok(Self::CPlus),
1049 "C" => Ok(Self::C),
1050 "C-" => Ok(Self::CMinus),
1051 "D" => Ok(Self::D),
1052 "F" => Ok(Self::F),
1053 _ => Err(GateConfigError::InvalidValue(format!(
1054 "Invalid grade: {s}. Valid values: A+, A, A-, B+, B, B-, C+, C, C-, D, F"
1055 ))),
1056 }
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 #[test]
1069 fn test_grade_from_percentage() {
1070 assert_eq!(Grade::from_percentage(100.0), Grade::APlus);
1071 assert_eq!(Grade::from_percentage(95.0), Grade::APlus);
1072 assert_eq!(Grade::from_percentage(92.0), Grade::A);
1073 assert_eq!(Grade::from_percentage(90.0), Grade::A);
1074 assert_eq!(Grade::from_percentage(87.0), Grade::AMinus);
1075 assert_eq!(Grade::from_percentage(85.0), Grade::AMinus);
1076 assert_eq!(Grade::from_percentage(82.0), Grade::BPlus);
1077 assert_eq!(Grade::from_percentage(80.0), Grade::BPlus);
1078 assert_eq!(Grade::from_percentage(77.0), Grade::B);
1079 assert_eq!(Grade::from_percentage(75.0), Grade::B);
1080 assert_eq!(Grade::from_percentage(72.0), Grade::BMinus);
1081 assert_eq!(Grade::from_percentage(70.0), Grade::BMinus);
1082 assert_eq!(Grade::from_percentage(67.0), Grade::CPlus);
1083 assert_eq!(Grade::from_percentage(65.0), Grade::CPlus);
1084 assert_eq!(Grade::from_percentage(62.0), Grade::C);
1085 assert_eq!(Grade::from_percentage(60.0), Grade::C);
1086 assert_eq!(Grade::from_percentage(57.0), Grade::CMinus);
1087 assert_eq!(Grade::from_percentage(55.0), Grade::CMinus);
1088 assert_eq!(Grade::from_percentage(52.0), Grade::D);
1089 assert_eq!(Grade::from_percentage(50.0), Grade::D);
1090 assert_eq!(Grade::from_percentage(49.0), Grade::F);
1091 assert_eq!(Grade::from_percentage(0.0), Grade::F);
1092 }
1093
1094 #[test]
1095 fn test_grade_min_percentage() {
1096 assert_eq!(Grade::APlus.min_percentage(), 95.0);
1097 assert_eq!(Grade::A.min_percentage(), 90.0);
1098 assert_eq!(Grade::AMinus.min_percentage(), 85.0);
1099 assert_eq!(Grade::BPlus.min_percentage(), 80.0);
1100 assert_eq!(Grade::B.min_percentage(), 75.0);
1101 assert_eq!(Grade::BMinus.min_percentage(), 70.0);
1102 assert_eq!(Grade::CPlus.min_percentage(), 65.0);
1103 assert_eq!(Grade::C.min_percentage(), 60.0);
1104 assert_eq!(Grade::CMinus.min_percentage(), 55.0);
1105 assert_eq!(Grade::D.min_percentage(), 50.0);
1106 assert_eq!(Grade::F.min_percentage(), 0.0);
1107 }
1108
1109 #[test]
1110 fn test_grade_letter() {
1111 assert_eq!(Grade::APlus.letter(), "A+");
1112 assert_eq!(Grade::A.letter(), "A");
1113 assert_eq!(Grade::AMinus.letter(), "A-");
1114 assert_eq!(Grade::BPlus.letter(), "B+");
1115 assert_eq!(Grade::B.letter(), "B");
1116 assert_eq!(Grade::BMinus.letter(), "B-");
1117 assert_eq!(Grade::CPlus.letter(), "C+");
1118 assert_eq!(Grade::C.letter(), "C");
1119 assert_eq!(Grade::CMinus.letter(), "C-");
1120 assert_eq!(Grade::D.letter(), "D");
1121 assert_eq!(Grade::F.letter(), "F");
1122 }
1123
1124 #[test]
1125 fn test_grade_is_passing() {
1126 assert!(Grade::APlus.is_passing());
1127 assert!(Grade::A.is_passing());
1128 assert!(Grade::AMinus.is_passing());
1129 assert!(Grade::BPlus.is_passing());
1130 assert!(Grade::B.is_passing());
1131 assert!(Grade::BMinus.is_passing());
1132 assert!(Grade::CPlus.is_passing());
1133 assert!(Grade::C.is_passing());
1134 assert!(!Grade::CMinus.is_passing());
1135 assert!(!Grade::D.is_passing());
1136 assert!(!Grade::F.is_passing());
1137 }
1138
1139 #[test]
1140 fn test_grade_is_production_ready() {
1141 assert!(Grade::APlus.is_production_ready());
1142 assert!(Grade::A.is_production_ready());
1143 assert!(Grade::AMinus.is_production_ready());
1144 assert!(Grade::BPlus.is_production_ready());
1145 assert!(!Grade::B.is_production_ready());
1146 assert!(!Grade::BMinus.is_production_ready());
1147 assert!(!Grade::F.is_production_ready());
1148 }
1149
1150 #[test]
1151 fn test_grade_default() {
1152 assert_eq!(Grade::default(), Grade::F);
1153 }
1154
1155 #[test]
1156 fn test_grade_display() {
1157 assert_eq!(format!("{}", Grade::APlus), "A+");
1158 assert_eq!(format!("{}", Grade::A), "A");
1159 assert_eq!(format!("{}", Grade::F), "F");
1160 }
1161
1162 #[test]
1163 fn test_grade_ordering() {
1164 assert!(Grade::APlus > Grade::A);
1165 assert!(Grade::A > Grade::AMinus);
1166 assert!(Grade::AMinus > Grade::BPlus);
1167 assert!(Grade::BPlus > Grade::B);
1168 assert!(Grade::B > Grade::BMinus);
1169 assert!(Grade::BMinus > Grade::CPlus);
1170 assert!(Grade::CPlus > Grade::C);
1171 assert!(Grade::C > Grade::CMinus);
1172 assert!(Grade::CMinus > Grade::D);
1173 assert!(Grade::D > Grade::F);
1174 }
1175
1176 #[test]
1181 fn test_criterion_new() {
1182 let c = Criterion::new("Test", "Description");
1183 assert_eq!(c.name, "Test");
1184 assert_eq!(c.description, "Description");
1185 assert_eq!(c.weight, 1.0);
1186 assert_eq!(c.score, 0.0);
1187 assert!(!c.passed);
1188 }
1189
1190 #[test]
1191 fn test_criterion_weight() {
1192 let c = Criterion::new("Test", "Desc").weight(0.5);
1193 assert_eq!(c.weight, 0.5);
1194 }
1195
1196 #[test]
1197 fn test_criterion_weight_clamped() {
1198 let c1 = Criterion::new("Test", "Desc").weight(2.0);
1199 assert_eq!(c1.weight, 1.0);
1200
1201 let c2 = Criterion::new("Test", "Desc").weight(-1.0);
1202 assert_eq!(c2.weight, 0.0);
1203 }
1204
1205 #[test]
1206 fn test_criterion_score() {
1207 let c = Criterion::new("Test", "Desc").score(85.0);
1208 assert_eq!(c.score, 85.0);
1209 assert!(c.passed);
1210 }
1211
1212 #[test]
1213 fn test_criterion_score_failing() {
1214 let c = Criterion::new("Test", "Desc").score(50.0);
1215 assert_eq!(c.score, 50.0);
1216 assert!(!c.passed);
1217 }
1218
1219 #[test]
1220 fn test_criterion_score_clamped() {
1221 let c1 = Criterion::new("Test", "Desc").score(150.0);
1222 assert_eq!(c1.score, 100.0);
1223
1224 let c2 = Criterion::new("Test", "Desc").score(-10.0);
1225 assert_eq!(c2.score, 0.0);
1226 }
1227
1228 #[test]
1229 fn test_criterion_pass() {
1230 let c = Criterion::new("Test", "Desc").pass();
1231 assert_eq!(c.score, 100.0);
1232 assert!(c.passed);
1233 }
1234
1235 #[test]
1236 fn test_criterion_fail() {
1237 let c = Criterion::new("Test", "Desc").fail();
1238 assert_eq!(c.score, 0.0);
1239 assert!(!c.passed);
1240 }
1241
1242 #[test]
1243 fn test_criterion_feedback() {
1244 let c = Criterion::new("Test", "Desc").feedback("Good work!");
1245 assert_eq!(c.feedback, Some("Good work!".to_string()));
1246 }
1247
1248 #[test]
1249 fn test_criterion_grade() {
1250 assert_eq!(Criterion::new("T", "D").score(95.0).grade(), Grade::APlus);
1251 assert_eq!(Criterion::new("T", "D").score(90.0).grade(), Grade::A);
1252 assert_eq!(Criterion::new("T", "D").score(85.0).grade(), Grade::AMinus);
1253 assert_eq!(Criterion::new("T", "D").score(80.0).grade(), Grade::BPlus);
1254 assert_eq!(Criterion::new("T", "D").score(75.0).grade(), Grade::B);
1255 assert_eq!(Criterion::new("T", "D").score(70.0).grade(), Grade::BMinus);
1256 assert_eq!(Criterion::new("T", "D").score(65.0).grade(), Grade::CPlus);
1257 assert_eq!(Criterion::new("T", "D").score(60.0).grade(), Grade::C);
1258 assert_eq!(Criterion::new("T", "D").score(55.0).grade(), Grade::CMinus);
1259 assert_eq!(Criterion::new("T", "D").score(50.0).grade(), Grade::D);
1260 assert_eq!(Criterion::new("T", "D").score(40.0).grade(), Grade::F);
1261 }
1262
1263 #[test]
1264 fn test_criterion_weighted_score() {
1265 let c = Criterion::new("Test", "Desc").weight(0.5).score(80.0);
1266 assert_eq!(c.weighted_score(), 40.0);
1267 }
1268
1269 #[test]
1274 fn test_report_card_new() {
1275 let report = ReportCard::new("My Report");
1276 assert_eq!(report.title, "My Report");
1277 assert!(report.criteria.is_empty());
1278 }
1279
1280 #[test]
1281 fn test_report_card_add_criterion() {
1282 let mut report = ReportCard::new("Test");
1283 report.add_criterion(Criterion::new("C1", "D1").score(90.0));
1284 assert_eq!(report.criteria.len(), 1);
1285 }
1286
1287 #[test]
1288 fn test_report_card_builder() {
1289 let report = ReportCard::new("Test")
1290 .criterion(Criterion::new("C1", "D1").score(90.0))
1291 .criterion(Criterion::new("C2", "D2").score(80.0));
1292 assert_eq!(report.criteria.len(), 2);
1293 }
1294
1295 #[test]
1296 fn test_report_card_overall_score_empty() {
1297 let report = ReportCard::new("Test");
1298 assert_eq!(report.overall_score(), 0.0);
1299 }
1300
1301 #[test]
1302 fn test_report_card_overall_score_equal_weights() {
1303 let report = ReportCard::new("Test")
1304 .criterion(Criterion::new("C1", "D1").weight(1.0).score(100.0))
1305 .criterion(Criterion::new("C2", "D2").weight(1.0).score(80.0));
1306 assert_eq!(report.overall_score(), 90.0);
1307 }
1308
1309 #[test]
1310 fn test_report_card_overall_score_different_weights() {
1311 let report = ReportCard::new("Test")
1312 .criterion(Criterion::new("C1", "D1").weight(0.75).score(100.0))
1313 .criterion(Criterion::new("C2", "D2").weight(0.25).score(80.0));
1314 assert_eq!(report.overall_score(), 95.0);
1316 }
1317
1318 #[test]
1319 fn test_report_card_overall_grade() {
1320 let report = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(90.0));
1321 assert_eq!(report.overall_grade(), Grade::A);
1322 }
1323
1324 #[test]
1325 fn test_report_card_all_passed() {
1326 let report = ReportCard::new("Test")
1327 .criterion(Criterion::new("C1", "D1").pass())
1328 .criterion(Criterion::new("C2", "D2").pass());
1329 assert!(report.all_passed());
1330 }
1331
1332 #[test]
1333 fn test_report_card_not_all_passed() {
1334 let report = ReportCard::new("Test")
1335 .criterion(Criterion::new("C1", "D1").pass())
1336 .criterion(Criterion::new("C2", "D2").fail());
1337 assert!(!report.all_passed());
1338 }
1339
1340 #[test]
1341 fn test_report_card_passed_count() {
1342 let report = ReportCard::new("Test")
1343 .criterion(Criterion::new("C1", "D1").pass())
1344 .criterion(Criterion::new("C2", "D2").pass())
1345 .criterion(Criterion::new("C3", "D3").fail());
1346 assert_eq!(report.passed_count(), 2);
1347 assert_eq!(report.failed_count(), 1);
1348 }
1349
1350 #[test]
1351 fn test_report_card_failures() {
1352 let report = ReportCard::new("Test")
1353 .criterion(Criterion::new("C1", "D1").pass())
1354 .criterion(Criterion::new("C2", "D2").fail());
1355 let failures = report.failures();
1356 assert_eq!(failures.len(), 1);
1357 assert_eq!(failures[0].name, "C2");
1358 }
1359
1360 #[test]
1361 fn test_report_card_is_passing() {
1362 let passing = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(90.0));
1363 assert!(passing.is_passing());
1364
1365 let failing = ReportCard::new("Test").criterion(Criterion::new("C1", "D1").score(50.0));
1366 assert!(!failing.is_passing());
1367 }
1368
1369 #[test]
1370 fn test_report_card_add_category() {
1371 let mut report = ReportCard::new("Test");
1372 report.add_category("performance", 95.0);
1373 assert_eq!(report.categories.get("performance"), Some(&95.0));
1374 }
1375
1376 #[test]
1381 fn test_evaluation_builder_new() {
1382 let builder = EvaluationBuilder::new("My Eval");
1383 let report = builder.build();
1384 assert_eq!(report.title, "My Eval");
1385 }
1386
1387 #[test]
1388 fn test_evaluation_builder_accessibility() {
1389 let report = EvaluationBuilder::new("Test")
1390 .accessibility(95.0, Some("Good a11y"))
1391 .build();
1392
1393 assert_eq!(report.criteria.len(), 1);
1394 assert_eq!(report.criteria[0].name, "Accessibility");
1395 assert_eq!(report.criteria[0].score, 95.0);
1396 assert_eq!(
1397 report.categories.get(categories::ACCESSIBILITY),
1398 Some(&95.0)
1399 );
1400 }
1401
1402 #[test]
1403 fn test_evaluation_builder_performance() {
1404 let report = EvaluationBuilder::new("Test")
1405 .performance(88.0, None)
1406 .build();
1407
1408 assert_eq!(report.criteria[0].name, "Performance");
1409 assert_eq!(report.criteria[0].score, 88.0);
1410 }
1411
1412 #[test]
1413 fn test_evaluation_builder_full() {
1414 let report = EvaluationBuilder::new("Full Evaluation")
1415 .accessibility(95.0, None)
1416 .performance(90.0, None)
1417 .visual(85.0, None)
1418 .code_quality(92.0, None)
1419 .testing(98.0, None)
1420 .build();
1421
1422 assert_eq!(report.criteria.len(), 5);
1423 assert!(report.overall_score() > 90.0);
1424 assert_eq!(report.overall_grade(), Grade::A);
1425 }
1426
1427 #[test]
1428 fn test_evaluation_builder_custom() {
1429 let report = EvaluationBuilder::new("Test")
1430 .custom(Criterion::new("Custom", "My custom criterion").score(75.0))
1431 .build();
1432
1433 assert_eq!(report.criteria[0].name, "Custom");
1434 }
1435
1436 #[test]
1441 fn test_score_breakdown_default() {
1442 let breakdown = ScoreBreakdown::default();
1443 assert_eq!(breakdown.total(), 0.0);
1444 }
1445
1446 #[test]
1447 fn test_score_breakdown_perfect() {
1448 let breakdown = ScoreBreakdown {
1449 widget_complexity: 100.0,
1450 layout_depth: 100.0,
1451 component_count: 100.0,
1452 render_time_p95: 100.0,
1453 memory_usage: 100.0,
1454 bundle_size: 100.0,
1455 wcag_aa_compliance: 100.0,
1456 keyboard_navigation: 100.0,
1457 screen_reader: 100.0,
1458 data_completeness: 100.0,
1459 data_freshness: 100.0,
1460 schema_validation: 100.0,
1461 manifest_completeness: 100.0,
1462 card_coverage: 100.0,
1463 theme_adherence: 100.0,
1464 naming_conventions: 100.0,
1465 };
1466
1467 assert!((breakdown.total() - 100.0).abs() < 0.01);
1468 }
1469
1470 #[test]
1471 fn test_score_breakdown_category_scores() {
1472 let breakdown = ScoreBreakdown {
1473 widget_complexity: 90.0,
1474 layout_depth: 90.0,
1475 component_count: 90.0,
1476 render_time_p95: 80.0,
1477 memory_usage: 80.0,
1478 bundle_size: 80.0,
1479 wcag_aa_compliance: 100.0,
1480 keyboard_navigation: 100.0,
1481 screen_reader: 100.0,
1482 data_completeness: 70.0,
1483 data_freshness: 70.0,
1484 schema_validation: 70.0,
1485 manifest_completeness: 60.0,
1486 card_coverage: 60.0,
1487 theme_adherence: 50.0,
1488 naming_conventions: 50.0,
1489 };
1490
1491 assert!((breakdown.structural_score() - 22.5).abs() < 0.01);
1493 assert!((breakdown.performance_score() - 16.0).abs() < 0.01);
1495 assert!((breakdown.accessibility_score() - 20.0).abs() < 0.01);
1497 }
1498
1499 #[test]
1500 fn test_app_quality_score_from_breakdown() {
1501 let breakdown = ScoreBreakdown {
1502 widget_complexity: 90.0,
1503 layout_depth: 90.0,
1504 component_count: 90.0,
1505 render_time_p95: 90.0,
1506 memory_usage: 90.0,
1507 bundle_size: 90.0,
1508 wcag_aa_compliance: 90.0,
1509 keyboard_navigation: 90.0,
1510 screen_reader: 90.0,
1511 data_completeness: 90.0,
1512 data_freshness: 90.0,
1513 schema_validation: 90.0,
1514 manifest_completeness: 90.0,
1515 card_coverage: 90.0,
1516 theme_adherence: 90.0,
1517 naming_conventions: 90.0,
1518 };
1519
1520 let score = AppQualityScore::from_breakdown(breakdown);
1521 assert!((score.overall - 90.0).abs() < 0.01);
1522 assert_eq!(score.grade, Grade::A);
1523 }
1524
1525 #[test]
1526 fn test_app_quality_score_meets_minimum() {
1527 let score = QualityScoreBuilder::new()
1528 .structural(85.0, 85.0, 85.0)
1529 .performance(85.0, 85.0, 85.0)
1530 .accessibility(85.0, 85.0, 85.0)
1531 .data_quality(85.0, 85.0, 85.0)
1532 .documentation(85.0, 85.0)
1533 .consistency(85.0, 85.0)
1534 .build();
1535
1536 assert!(score.meets_minimum(Grade::BPlus));
1537 assert!(!score.meets_minimum(Grade::A));
1538 }
1539
1540 #[test]
1541 fn test_app_quality_score_production_ready() {
1542 let ready = QualityScoreBuilder::new()
1543 .structural(90.0, 90.0, 90.0)
1544 .performance(90.0, 90.0, 90.0)
1545 .accessibility(90.0, 90.0, 90.0)
1546 .data_quality(90.0, 90.0, 90.0)
1547 .documentation(90.0, 90.0)
1548 .consistency(90.0, 90.0)
1549 .build();
1550
1551 assert!(ready.is_production_ready());
1552
1553 let not_ready = QualityScoreBuilder::new()
1554 .structural(70.0, 70.0, 70.0)
1555 .performance(70.0, 70.0, 70.0)
1556 .accessibility(70.0, 70.0, 70.0)
1557 .data_quality(70.0, 70.0, 70.0)
1558 .documentation(70.0, 70.0)
1559 .consistency(70.0, 70.0)
1560 .build();
1561
1562 assert!(!not_ready.is_production_ready());
1563 }
1564
1565 #[test]
1566 fn test_quality_score_builder() {
1567 let score = QualityScoreBuilder::new()
1568 .structural(100.0, 100.0, 100.0)
1569 .performance(100.0, 100.0, 100.0)
1570 .accessibility(100.0, 100.0, 100.0)
1571 .data_quality(100.0, 100.0, 100.0)
1572 .documentation(100.0, 100.0)
1573 .consistency(100.0, 100.0)
1574 .build();
1575
1576 assert!((score.overall - 100.0).abs() < 0.01);
1577 assert_eq!(score.grade, Grade::APlus);
1578 }
1579
1580 #[test]
1581 fn test_quality_score_builder_clamping() {
1582 let score = QualityScoreBuilder::new()
1583 .structural(150.0, -10.0, 200.0)
1584 .build();
1585
1586 assert_eq!(score.breakdown.widget_complexity, 100.0);
1588 assert_eq!(score.breakdown.layout_depth, 0.0);
1589 assert_eq!(score.breakdown.component_count, 100.0);
1590 }
1591
1592 #[test]
1597 fn test_quality_gates_default() {
1598 let gates = QualityGates::default();
1599 assert_eq!(gates.min_grade, Grade::BPlus);
1600 assert_eq!(gates.min_score, 80.0);
1601 assert_eq!(gates.performance.max_render_time_ms, 16);
1602 assert_eq!(gates.accessibility.wcag_level, "AA");
1603 }
1604
1605 #[test]
1606 fn test_quality_gates_check_passes() {
1607 let gates = QualityGates::default();
1608 let score = QualityScoreBuilder::new()
1609 .structural(90.0, 90.0, 90.0)
1610 .performance(90.0, 90.0, 90.0)
1611 .accessibility(90.0, 90.0, 90.0)
1612 .data_quality(90.0, 90.0, 90.0)
1613 .documentation(90.0, 90.0)
1614 .consistency(90.0, 90.0)
1615 .build();
1616
1617 let result = gates.check(&score);
1618 assert!(result.passed);
1619 assert!(result.violations.is_empty());
1620 }
1621
1622 #[test]
1623 fn test_quality_gates_check_fails_grade() {
1624 let gates = QualityGates::default();
1625 let score = QualityScoreBuilder::new()
1626 .structural(60.0, 60.0, 60.0)
1627 .performance(60.0, 60.0, 60.0)
1628 .accessibility(60.0, 60.0, 60.0)
1629 .data_quality(60.0, 60.0, 60.0)
1630 .documentation(60.0, 60.0)
1631 .consistency(60.0, 60.0)
1632 .build();
1633
1634 let result = gates.check(&score);
1635 assert!(!result.passed);
1636 assert!(!result.violations.is_empty());
1637 assert_eq!(result.violations[0].gate, "min_grade");
1638 }
1639
1640 #[test]
1641 fn test_quality_gates_check_fails_score() {
1642 let mut gates = QualityGates::default();
1643 gates.min_grade = Grade::C; gates.min_score = 95.0; let score = QualityScoreBuilder::new()
1647 .structural(85.0, 85.0, 85.0)
1648 .performance(85.0, 85.0, 85.0)
1649 .accessibility(85.0, 85.0, 85.0)
1650 .data_quality(85.0, 85.0, 85.0)
1651 .documentation(85.0, 85.0)
1652 .consistency(85.0, 85.0)
1653 .build();
1654
1655 let result = gates.check(&score);
1656 assert!(!result.passed);
1657 assert!(result.violations.iter().any(|v| v.gate == "min_score"));
1658 }
1659
1660 #[test]
1661 fn test_performance_gates_default() {
1662 let gates = PerformanceGates::default();
1663 assert_eq!(gates.max_render_time_ms, 16);
1664 assert_eq!(gates.max_bundle_size_kb, 500);
1665 assert_eq!(gates.max_memory_mb, 100);
1666 }
1667
1668 #[test]
1669 fn test_accessibility_gates_default() {
1670 let gates = AccessibilityGates::default();
1671 assert_eq!(gates.wcag_level, "AA");
1672 assert_eq!(gates.min_contrast_ratio, 4.5);
1673 assert!(gates.require_keyboard_nav);
1674 assert!(gates.require_aria_labels);
1675 }
1676
1677 #[test]
1678 fn test_documentation_gates_default() {
1679 let gates = DocumentationGates::default();
1680 assert!(gates.require_model_cards);
1681 assert!(gates.require_data_cards);
1682 assert!(gates.min_manifest_fields.contains(&"name".to_string()));
1683 assert!(gates.min_manifest_fields.contains(&"version".to_string()));
1684 }
1685
1686 #[test]
1687 fn test_violation_severity() {
1688 let violation = GateViolation {
1689 gate: "test".to_string(),
1690 expected: "A".to_string(),
1691 actual: "B".to_string(),
1692 severity: ViolationSeverity::Error,
1693 };
1694 assert_eq!(violation.severity, ViolationSeverity::Error);
1695 }
1696
1697 #[test]
1702 fn test_gate_config_error_display() {
1703 let err = GateConfigError::ParseError("invalid toml".to_string());
1704 assert!(err.to_string().contains("parse error"));
1705
1706 let err = GateConfigError::IoError("file not found".to_string());
1707 assert!(err.to_string().contains("IO error"));
1708
1709 let err = GateConfigError::InvalidValue("out of range".to_string());
1710 assert!(err.to_string().contains("invalid value"));
1711 }
1712
1713 #[test]
1714 fn test_quality_gates_to_toml() {
1715 let gates = QualityGates::default();
1716 let toml_str = gates.to_toml();
1717
1718 assert!(toml_str.contains("min_score"));
1719 assert!(toml_str.contains("[performance]"));
1720 assert!(toml_str.contains("[accessibility]"));
1721 assert!(toml_str.contains("max_render_time_ms"));
1722 }
1723
1724 #[test]
1725 fn test_quality_gates_from_toml() {
1726 let toml_str = r#"
1727 min_grade = "A"
1728 min_score = 90.0
1729
1730 [performance]
1731 max_render_time_ms = 8
1732 max_bundle_size_kb = 300
1733 max_memory_mb = 50
1734
1735 [accessibility]
1736 wcag_level = "AAA"
1737 min_contrast_ratio = 7.0
1738 require_keyboard_nav = true
1739 require_aria_labels = true
1740
1741 [data]
1742 max_staleness_minutes = 30
1743 require_schema_validation = true
1744
1745 [documentation]
1746 require_model_cards = true
1747 require_data_cards = true
1748 min_manifest_fields = ["name", "version"]
1749 "#;
1750
1751 let gates = QualityGates::from_toml(toml_str).unwrap();
1752 assert_eq!(gates.min_score, 90.0);
1753 assert_eq!(gates.performance.max_render_time_ms, 8);
1754 assert_eq!(gates.performance.max_bundle_size_kb, 300);
1755 assert_eq!(gates.accessibility.wcag_level, "AAA");
1756 assert_eq!(gates.accessibility.min_contrast_ratio, 7.0);
1757 assert_eq!(gates.data.max_staleness_minutes, 30);
1758 }
1759
1760 #[test]
1761 fn test_quality_gates_roundtrip() {
1762 let original = QualityGates::default();
1763 let toml_str = original.to_toml();
1764 let parsed = QualityGates::from_toml(&toml_str).unwrap();
1765
1766 assert_eq!(parsed.min_score, original.min_score);
1767 assert_eq!(
1768 parsed.performance.max_render_time_ms,
1769 original.performance.max_render_time_ms
1770 );
1771 assert_eq!(
1772 parsed.accessibility.wcag_level,
1773 original.accessibility.wcag_level
1774 );
1775 }
1776
1777 #[test]
1778 fn test_quality_gates_from_toml_invalid() {
1779 let result = QualityGates::from_toml("this is not valid toml {{{");
1780 assert!(matches!(result, Err(GateConfigError::ParseError(_))));
1781 }
1782
1783 #[test]
1784 fn test_quality_gates_sample_config() {
1785 let sample = QualityGates::sample_config();
1786 assert!(sample.contains("min_grade"));
1787 assert!(sample.contains("max_bundle_size_kb"));
1788 assert!(sample.contains("wcag_level"));
1789 assert!(sample.contains("[performance]"));
1790 assert!(sample.contains("[accessibility]"));
1791 assert!(sample.contains("[data]"));
1792 assert!(sample.contains("[documentation]"));
1793 }
1794
1795 #[test]
1796 fn test_quality_gates_check_extended_passes() {
1797 let gates = QualityGates::default();
1798 let score = QualityScoreBuilder::new()
1799 .structural(90.0, 90.0, 90.0)
1800 .performance(90.0, 90.0, 90.0)
1801 .accessibility(90.0, 90.0, 90.0)
1802 .data_quality(90.0, 90.0, 90.0)
1803 .documentation(90.0, 90.0)
1804 .consistency(90.0, 90.0)
1805 .build();
1806
1807 let result = gates.check_extended(&score, Some(10), Some(400), Some(50));
1808 assert!(result.passed);
1809 assert!(result.violations.is_empty());
1810 }
1811
1812 #[test]
1813 fn test_quality_gates_check_extended_render_time_fails() {
1814 let gates = QualityGates::default();
1815 let score = QualityScoreBuilder::new()
1816 .structural(90.0, 90.0, 90.0)
1817 .performance(90.0, 90.0, 90.0)
1818 .accessibility(90.0, 90.0, 90.0)
1819 .data_quality(90.0, 90.0, 90.0)
1820 .documentation(90.0, 90.0)
1821 .consistency(90.0, 90.0)
1822 .build();
1823
1824 let result = gates.check_extended(&score, Some(25), Some(400), Some(50));
1826 assert!(!result.passed);
1827 assert!(result
1828 .violations
1829 .iter()
1830 .any(|v| v.gate == "max_render_time_ms"));
1831 }
1832
1833 #[test]
1834 fn test_quality_gates_check_extended_bundle_size_fails() {
1835 let gates = QualityGates::default();
1836 let score = QualityScoreBuilder::new()
1837 .structural(90.0, 90.0, 90.0)
1838 .performance(90.0, 90.0, 90.0)
1839 .accessibility(90.0, 90.0, 90.0)
1840 .data_quality(90.0, 90.0, 90.0)
1841 .documentation(90.0, 90.0)
1842 .consistency(90.0, 90.0)
1843 .build();
1844
1845 let result = gates.check_extended(&score, Some(10), Some(600), Some(50));
1847 assert!(!result.passed);
1848 assert!(result
1849 .violations
1850 .iter()
1851 .any(|v| v.gate == "max_bundle_size_kb"));
1852 }
1853
1854 #[test]
1855 fn test_quality_gates_check_extended_memory_warning() {
1856 let gates = QualityGates::default();
1857 let score = QualityScoreBuilder::new()
1858 .structural(90.0, 90.0, 90.0)
1859 .performance(90.0, 90.0, 90.0)
1860 .accessibility(90.0, 90.0, 90.0)
1861 .data_quality(90.0, 90.0, 90.0)
1862 .documentation(90.0, 90.0)
1863 .consistency(90.0, 90.0)
1864 .build();
1865
1866 let result = gates.check_extended(&score, Some(10), Some(400), Some(150));
1868 assert!(result.passed); assert!(result.violations.iter().any(|v| v.gate == "max_memory_mb"));
1870 assert!(result
1871 .violations
1872 .iter()
1873 .any(|v| v.severity == ViolationSeverity::Warning));
1874 }
1875
1876 #[test]
1877 fn test_grade_from_str() {
1878 assert_eq!(Grade::from_str("A+").unwrap(), Grade::APlus);
1879 assert_eq!(Grade::from_str("a+").unwrap(), Grade::APlus);
1880 assert_eq!(Grade::from_str("A").unwrap(), Grade::A);
1881 assert_eq!(Grade::from_str("A-").unwrap(), Grade::AMinus);
1882 assert_eq!(Grade::from_str("B+").unwrap(), Grade::BPlus);
1883 assert_eq!(Grade::from_str("B").unwrap(), Grade::B);
1884 assert_eq!(Grade::from_str("B-").unwrap(), Grade::BMinus);
1885 assert_eq!(Grade::from_str("C+").unwrap(), Grade::CPlus);
1886 assert_eq!(Grade::from_str("C").unwrap(), Grade::C);
1887 assert_eq!(Grade::from_str("C-").unwrap(), Grade::CMinus);
1888 assert_eq!(Grade::from_str("D").unwrap(), Grade::D);
1889 assert_eq!(Grade::from_str("F").unwrap(), Grade::F);
1890 }
1891
1892 #[test]
1893 fn test_grade_from_str_invalid() {
1894 assert!(matches!(
1895 Grade::from_str("X"),
1896 Err(GateConfigError::InvalidValue(_))
1897 ));
1898 assert!(matches!(
1899 Grade::from_str("E"),
1900 Err(GateConfigError::InvalidValue(_))
1901 ));
1902 assert!(matches!(
1903 Grade::from_str(""),
1904 Err(GateConfigError::InvalidValue(_))
1905 ));
1906 }
1907
1908 #[test]
1909 fn test_quality_gates_config_file_constant() {
1910 assert_eq!(QualityGates::CONFIG_FILE, ".presentar-gates.toml");
1911 }
1912}