Skip to main content

presentar_test/
grade.rs

1#![allow(
2    clippy::derive_partial_eq_without_eq,
3    clippy::doc_markdown,
4    clippy::missing_const_for_fn
5)]
6//! Grade scoring system for quality evaluation.
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Quality grade levels (A+ through F) per spec.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
13pub enum Grade {
14    /// Failing (0-49)
15    #[default]
16    F,
17    /// Incomplete (50-54)
18    D,
19    /// Sketch (55-59)
20    CMinus,
21    /// Draft (60-64)
22    C,
23    /// Prototype (65-69)
24    CPlus,
25    /// Development (70-74)
26    BMinus,
27    /// Alpha Quality (75-79)
28    B,
29    /// Beta Quality (80-84)
30    BPlus,
31    /// Release Candidate (85-89)
32    AMinus,
33    /// Production Ready (90-94)
34    A,
35    /// Production Excellence (95-100)
36    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        // Compare by minimum percentage (A > B > C > D > F)
48        self.min_percentage()
49            .partial_cmp(&other.min_percentage())
50            .unwrap_or(std::cmp::Ordering::Equal)
51    }
52}
53
54impl Grade {
55    /// Create a grade from a percentage score (0-100 scale).
56    #[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    /// Get the minimum percentage for this grade.
74    #[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    /// Get grade as a letter string.
92    #[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    /// Check if this is a passing grade (C or better = 60+).
110    #[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    /// Check if this grade is production ready (B+ or better = 80+).
126    #[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/// A scored criterion with name, weight, and result.
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Criterion {
141    /// Name of the criterion
142    pub name: String,
143    /// Description of what is being measured
144    pub description: String,
145    /// Weight (importance) of this criterion (0.0 - 1.0)
146    pub weight: f32,
147    /// Score achieved (0.0 - 100.0)
148    pub score: f32,
149    /// Whether this criterion passed
150    pub passed: bool,
151    /// Detailed feedback
152    pub feedback: Option<String>,
153}
154
155impl Criterion {
156    /// Create a new criterion.
157    #[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    /// Set the weight.
170    #[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    /// Set the score.
177    #[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    /// Mark as passed with a perfect score.
185    #[must_use]
186    pub const fn pass(mut self) -> Self {
187        self.score = 100.0;
188        self.passed = true;
189        self
190    }
191
192    /// Mark as failed with zero score.
193    #[must_use]
194    pub const fn fail(mut self) -> Self {
195        self.score = 0.0;
196        self.passed = false;
197        self
198    }
199
200    /// Add feedback.
201    #[must_use]
202    pub fn feedback(mut self, feedback: impl Into<String>) -> Self {
203        self.feedback = Some(feedback.into());
204        self
205    }
206
207    /// Get the grade for this criterion.
208    #[must_use]
209    pub fn grade(&self) -> Grade {
210        Grade::from_percentage(self.score)
211    }
212
213    /// Get weighted score (score * weight).
214    #[must_use]
215    pub fn weighted_score(&self) -> f32 {
216        self.score * self.weight
217    }
218}
219
220/// A report card containing multiple criteria scores.
221#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
222pub struct ReportCard {
223    /// Title of the evaluation
224    pub title: String,
225    /// Individual criteria scores
226    pub criteria: Vec<Criterion>,
227    /// Category scores (aggregated)
228    pub categories: HashMap<String, f32>,
229}
230
231impl ReportCard {
232    /// Create a new report card.
233    #[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    /// Add a criterion.
243    pub fn add_criterion(&mut self, criterion: Criterion) {
244        self.criteria.push(criterion);
245    }
246
247    /// Add criterion with builder pattern.
248    #[must_use]
249    pub fn criterion(mut self, criterion: Criterion) -> Self {
250        self.criteria.push(criterion);
251        self
252    }
253
254    /// Add a category score.
255    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    /// Calculate the overall weighted score.
260    #[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    /// Get the overall grade.
276    #[must_use]
277    pub fn overall_grade(&self) -> Grade {
278        Grade::from_percentage(self.overall_score())
279    }
280
281    /// Check if all criteria passed.
282    #[must_use]
283    pub fn all_passed(&self) -> bool {
284        self.criteria.iter().all(|c| c.passed)
285    }
286
287    /// Count passed criteria.
288    #[must_use]
289    pub fn passed_count(&self) -> usize {
290        self.criteria.iter().filter(|c| c.passed).count()
291    }
292
293    /// Count failed criteria.
294    #[must_use]
295    pub fn failed_count(&self) -> usize {
296        self.criteria.iter().filter(|c| !c.passed).count()
297    }
298
299    /// Get all failing criteria.
300    #[must_use]
301    pub fn failures(&self) -> Vec<&Criterion> {
302        self.criteria.iter().filter(|c| !c.passed).collect()
303    }
304
305    /// Check if the overall grade is passing.
306    #[must_use]
307    pub fn is_passing(&self) -> bool {
308        self.overall_grade().is_passing()
309    }
310}
311
312/// Standard evaluation categories.
313pub mod categories {
314    /// Accessibility evaluation.
315    pub const ACCESSIBILITY: &str = "accessibility";
316    /// Performance evaluation.
317    pub const PERFORMANCE: &str = "performance";
318    /// Visual consistency.
319    pub const VISUAL: &str = "visual";
320    /// Code quality.
321    pub const CODE_QUALITY: &str = "code_quality";
322    /// Test coverage.
323    pub const TESTING: &str = "testing";
324    /// Documentation.
325    pub const DOCUMENTATION: &str = "documentation";
326    /// Security.
327    pub const SECURITY: &str = "security";
328}
329
330// =============================================================================
331// AppQualityScore - Per Spec Section 5.1
332// =============================================================================
333
334/// App Quality Score breakdown per spec (6 orthogonal metrics).
335#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
336pub struct ScoreBreakdown {
337    // Structural (25 points)
338    /// Cyclomatic complexity (McCabe, 1976)
339    pub widget_complexity: f64,
340    /// Nesting depth penalty
341    pub layout_depth: f64,
342    /// Widget count vs viewport
343    pub component_count: f64,
344
345    // Performance (20 points)
346    /// 95th percentile frame time
347    pub render_time_p95: f64,
348    /// Peak memory vs baseline
349    pub memory_usage: f64,
350    /// WASM binary size
351    pub bundle_size: f64,
352
353    // Accessibility (20 points)
354    /// WCAG 2.1 AA checklist
355    pub wcag_aa_compliance: f64,
356    /// Full keyboard support
357    pub keyboard_navigation: f64,
358    /// ARIA labels coverage
359    pub screen_reader: f64,
360
361    // Data Quality (15 points)
362    /// Missing value ratio
363    pub data_completeness: f64,
364    /// Staleness penalty
365    pub data_freshness: f64,
366    /// Type errors
367    pub schema_validation: f64,
368
369    // Documentation (10 points)
370    /// Required fields coverage
371    pub manifest_completeness: f64,
372    /// Model/data cards present
373    pub card_coverage: f64,
374
375    // Consistency (10 points)
376    /// Design system compliance
377    pub theme_adherence: f64,
378    /// ID/class naming
379    pub naming_conventions: f64,
380}
381
382impl ScoreBreakdown {
383    /// Calculate structural score (contributes 25% to total).
384    #[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    /// Calculate performance score (contributes 20% to total).
390    #[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    /// Calculate accessibility score (contributes 20% to total).
396    #[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    /// Calculate data quality score (contributes 15% to total).
402    #[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    /// Calculate documentation score (contributes 10% to total).
408    #[must_use]
409    pub fn documentation_score(&self) -> f64 {
410        (self.manifest_completeness + self.card_coverage) / 2.0 * 0.10
411    }
412
413    /// Calculate consistency score (contributes 10% to total).
414    #[must_use]
415    pub fn consistency_score(&self) -> f64 {
416        (self.theme_adherence + self.naming_conventions) / 2.0 * 0.10
417    }
418
419    /// Calculate total score (0-100).
420    #[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/// App Quality Score (0-100, F-A+).
432#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
433pub struct AppQualityScore {
434    /// Overall score (0-100)
435    pub overall: f64,
436    /// Grade (F through A+)
437    pub grade: Grade,
438    /// Detailed breakdown
439    pub breakdown: ScoreBreakdown,
440}
441
442impl AppQualityScore {
443    /// Create from a score breakdown.
444    #[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    /// Check if score meets minimum grade requirement.
456    #[must_use]
457    pub fn meets_minimum(&self, min_grade: Grade) -> bool {
458        self.grade >= min_grade
459    }
460
461    /// Check if production ready (B+ or better).
462    #[must_use]
463    pub fn is_production_ready(&self) -> bool {
464        self.grade.is_production_ready()
465    }
466}
467
468/// Builder for constructing AppQualityScore.
469#[derive(Debug, Clone, Default)]
470pub struct QualityScoreBuilder {
471    breakdown: ScoreBreakdown,
472}
473
474impl QualityScoreBuilder {
475    /// Create a new quality score builder.
476    #[must_use]
477    pub fn new() -> Self {
478        Self::default()
479    }
480
481    /// Set structural metrics.
482    #[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    /// Set performance metrics.
491    #[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    /// Set accessibility metrics.
500    #[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    /// Set data quality metrics.
509    #[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    /// Set documentation metrics.
518    #[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    /// Set consistency metrics.
526    #[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    /// Build the final quality score.
534    #[must_use]
535    pub fn build(self) -> AppQualityScore {
536        AppQualityScore::from_breakdown(self.breakdown)
537    }
538}
539
540// =============================================================================
541// QualityGates - Per Spec Section 5.3
542// =============================================================================
543
544/// Quality gate configuration (from .presentar-gates.toml).
545#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
546pub struct QualityGates {
547    /// Minimum required grade
548    pub min_grade: Grade,
549    /// Minimum required score (0-100)
550    pub min_score: f64,
551    /// Performance requirements
552    pub performance: PerformanceGates,
553    /// Accessibility requirements
554    pub accessibility: AccessibilityGates,
555    /// Data requirements
556    pub data: DataGates,
557    /// Documentation requirements
558    pub documentation: DocumentationGates,
559}
560
561/// Performance quality gates.
562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
563pub struct PerformanceGates {
564    /// Maximum render time in milliseconds (60fps = 16ms)
565    pub max_render_time_ms: u32,
566    /// Maximum bundle size in KB
567    pub max_bundle_size_kb: u32,
568    /// Maximum memory usage in MB
569    pub max_memory_mb: u32,
570}
571
572/// Accessibility quality gates.
573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
574pub struct AccessibilityGates {
575    /// WCAG level ("A", "AA", "AAA")
576    pub wcag_level: String,
577    /// Minimum contrast ratio
578    pub min_contrast_ratio: f32,
579    /// Require full keyboard navigation
580    pub require_keyboard_nav: bool,
581    /// Require ARIA labels
582    pub require_aria_labels: bool,
583}
584
585/// Data quality gates.
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct DataGates {
588    /// Maximum staleness in minutes
589    pub max_staleness_minutes: u32,
590    /// Require schema validation
591    pub require_schema_validation: bool,
592}
593
594/// Documentation quality gates.
595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
596pub struct DocumentationGates {
597    /// Require model cards
598    pub require_model_cards: bool,
599    /// Require data cards
600    pub require_data_cards: bool,
601    /// Minimum manifest fields
602    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/// Result of checking quality gates.
663#[derive(Debug, Clone)]
664pub struct GateCheckResult {
665    /// Whether all gates passed
666    pub passed: bool,
667    /// List of gate violations
668    pub violations: Vec<GateViolation>,
669}
670
671/// A single gate violation.
672#[derive(Debug, Clone)]
673pub struct GateViolation {
674    /// Gate name
675    pub gate: String,
676    /// Expected value
677    pub expected: String,
678    /// Actual value
679    pub actual: String,
680    /// Severity
681    pub severity: ViolationSeverity,
682}
683
684/// Severity of a gate violation.
685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum ViolationSeverity {
687    /// Warning - gate not met but not blocking
688    Warning,
689    /// Error - gate not met and blocking
690    Error,
691}
692
693impl QualityGates {
694    /// Check if a quality score passes all gates.
695    #[must_use]
696    pub fn check(&self, score: &AppQualityScore) -> GateCheckResult {
697        let mut violations = Vec::new();
698
699        // Check minimum grade
700        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        // Check minimum score
710        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/// Builder for creating standard evaluation criteria.
727#[derive(Debug, Clone, Default)]
728pub struct EvaluationBuilder {
729    report: ReportCard,
730}
731
732impl EvaluationBuilder {
733    /// Create a new evaluation builder.
734    #[must_use]
735    pub fn new(title: impl Into<String>) -> Self {
736        Self {
737            report: ReportCard::new(title),
738        }
739    }
740
741    /// Add accessibility criterion.
742    #[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    /// Add performance criterion.
762    #[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    /// Add visual criterion.
782    #[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    /// Add code quality criterion.
800    #[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    /// Add testing criterion.
820    #[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    /// Add custom criterion.
837    #[must_use]
838    pub fn custom(mut self, criterion: Criterion) -> Self {
839        self.report.add_criterion(criterion);
840        self
841    }
842
843    /// Build the final report card.
844    #[must_use]
845    pub fn build(self) -> ReportCard {
846        self.report
847    }
848}
849
850// =============================================================================
851// TOML Configuration Loading
852// =============================================================================
853
854/// Error type for quality gate configuration.
855#[derive(Debug, Clone, PartialEq)]
856pub enum GateConfigError {
857    /// Failed to parse TOML
858    ParseError(String),
859    /// Failed to read file
860    IoError(String),
861    /// Invalid configuration value
862    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    /// Default config file name.
879    pub const CONFIG_FILE: &'static str = ".presentar-gates.toml";
880
881    /// Parse quality gates from a TOML string.
882    ///
883    /// # Errors
884    ///
885    /// Returns error if TOML is invalid or values are out of range.
886    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    /// Serialize quality gates to a TOML string.
891    #[must_use]
892    pub fn to_toml(&self) -> String {
893        toml::to_string_pretty(self).unwrap_or_default()
894    }
895
896    /// Load quality gates from a file.
897    ///
898    /// # Errors
899    ///
900    /// Returns error if file cannot be read or parsed.
901    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    /// Save quality gates to a file.
908    ///
909    /// # Errors
910    ///
911    /// Returns error if file cannot be written.
912    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    /// Load from default config file in current directory.
918    ///
919    /// Returns default config if file doesn't exist.
920    #[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    /// Check a score with extended validation (performance, bundle size, etc.).
927    #[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        // Check performance gates
938        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    /// Generate a sample TOML config file content.
979    #[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    /// Parse grade from string (e.g., "A+", "B-", "C").
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns error if string is not a valid grade.
1040    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    // =========================================================================
1065    // Grade Tests - TESTS FIRST
1066    // =========================================================================
1067
1068    #[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    // =========================================================================
1177    // Criterion Tests - TESTS FIRST
1178    // =========================================================================
1179
1180    #[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    // =========================================================================
1270    // ReportCard Tests - TESTS FIRST
1271    // =========================================================================
1272
1273    #[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        // (100*0.75 + 80*0.25) / (0.75 + 0.25) = (75 + 20) / 1.0 = 95
1315        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    // =========================================================================
1377    // EvaluationBuilder Tests - TESTS FIRST
1378    // =========================================================================
1379
1380    #[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    // =========================================================================
1437    // AppQualityScore Tests - TESTS FIRST
1438    // =========================================================================
1439
1440    #[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        // structural: 90 * 0.25 = 22.5
1492        assert!((breakdown.structural_score() - 22.5).abs() < 0.01);
1493        // performance: 80 * 0.20 = 16.0
1494        assert!((breakdown.performance_score() - 16.0).abs() < 0.01);
1495        // accessibility: 100 * 0.20 = 20.0
1496        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        // Values should be clamped to 0-100
1587        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    // =========================================================================
1593    // QualityGates Tests
1594    // =========================================================================
1595
1596    #[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; // Lower grade threshold
1644        gates.min_score = 95.0; // But require high score
1645
1646        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    // =========================================================================
1698    // TOML Configuration Tests
1699    // =========================================================================
1700
1701    #[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        // Render time exceeds 16ms limit
1825        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        // Bundle size exceeds 500KB limit
1846        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        // Memory exceeds limit but is only a warning
1867        let result = gates.check_extended(&score, Some(10), Some(400), Some(150));
1868        assert!(result.passed); // Memory is a warning, not error
1869        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}