Skip to main content

batuta/stack/quality/
types.rs

1//! Quality type definitions
2//!
3//! Contains core types for quality assessment:
4//! - `QualityGrade` - Grade levels (A+ through F)
5//! - `Score` - Score with context
6//! - `IssueSeverity` - Issue severity levels
7//! - `QualityIssue` - Quality issues found during analysis
8//! - `StackLayer` - Layer in the PAIML stack architecture
9
10use serde::{Deserialize, Serialize};
11
12// ============================================================================
13// Quality Grade
14// ============================================================================
15
16/// Quality grade levels based on score percentages
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18pub enum QualityGrade {
19    /// A+ (95-100% of max score)
20    APlus,
21    /// A (90-94%)
22    A,
23    /// A- (85-89%) - PMAT minimum for production
24    AMinus,
25    /// B+ (80-84%)
26    BPlus,
27    /// B (70-79%)
28    B,
29    /// C (60-69%)
30    C,
31    /// D (50-59%)
32    D,
33    /// F (0-49%)
34    F,
35}
36
37impl QualityGrade {
38    /// Calculate grade from Rust project score (max 114)
39    pub fn from_rust_project_score(score: u32) -> Self {
40        match score {
41            105..=114 => Self::APlus,
42            95..=104 => Self::A,
43            85..=94 => Self::AMinus,
44            80..=84 => Self::BPlus,
45            70..=79 => Self::B,
46            60..=69 => Self::C,
47            50..=59 => Self::D,
48            _ => Self::F,
49        }
50    }
51
52    /// Calculate grade from repository score (max 110)
53    pub fn from_repo_score(score: u32) -> Self {
54        match score {
55            95..=110 => Self::APlus,
56            90..=94 => Self::A,
57            85..=89 => Self::AMinus,
58            80..=84 => Self::BPlus,
59            70..=79 => Self::B,
60            60..=69 => Self::C,
61            50..=59 => Self::D,
62            _ => Self::F,
63        }
64    }
65
66    /// Calculate grade from README score (max 20)
67    pub fn from_readme_score(score: u32) -> Self {
68        match score {
69            18..=20 => Self::APlus,
70            16..=17 => Self::A,
71            14..=15 => Self::AMinus,
72            12..=13 => Self::BPlus,
73            10..=11 => Self::B,
74            8..=9 => Self::C,
75            6..=7 => Self::D,
76            _ => Self::F,
77        }
78    }
79
80    /// Calculate grade from Stack Quality Index (0-100)
81    pub fn from_sqi(sqi: f64) -> Self {
82        match sqi as u32 {
83            95..=100 => Self::APlus,
84            90..=94 => Self::A,
85            85..=89 => Self::AMinus,
86            80..=84 => Self::BPlus,
87            70..=79 => Self::B,
88            60..=69 => Self::C,
89            50..=59 => Self::D,
90            _ => Self::F,
91        }
92    }
93
94    /// Check if grade is release-ready (A- or better)
95    pub fn is_release_ready(&self) -> bool {
96        matches!(self, Self::APlus | Self::A | Self::AMinus)
97    }
98
99    /// Check if grade is A+
100    pub fn is_a_plus(&self) -> bool {
101        matches!(self, Self::APlus)
102    }
103
104    /// Get display symbol for grade
105    pub fn symbol(&self) -> &'static str {
106        match self {
107            Self::APlus => "A+",
108            Self::A => "A",
109            Self::AMinus => "A-",
110            Self::BPlus => "B+",
111            Self::B => "B",
112            Self::C => "C",
113            Self::D => "D",
114            Self::F => "F",
115        }
116    }
117
118    /// Get status icon for grade
119    pub fn icon(&self) -> &'static str {
120        match self {
121            Self::APlus => "✅",
122            Self::A | Self::AMinus => "⚠️",
123            _ => "❌",
124        }
125    }
126}
127
128impl std::fmt::Display for QualityGrade {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(f, "{}", self.symbol())
131    }
132}
133
134// ============================================================================
135// Score Types
136// ============================================================================
137
138/// Score with context
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Score {
141    /// Actual score value
142    pub value: u32,
143    /// Maximum possible score
144    pub max: u32,
145    /// Calculated grade
146    pub grade: QualityGrade,
147}
148
149impl Score {
150    /// Create a new score
151    pub fn new(value: u32, max: u32, grade: QualityGrade) -> Self {
152        Self { value, max, grade }
153    }
154
155    /// Calculate percentage
156    pub fn percentage(&self) -> f64 {
157        if self.max == 0 {
158            0.0
159        } else {
160            (self.value as f64 / self.max as f64) * 100.0
161        }
162    }
163
164    /// Normalize to 0-100 scale
165    pub fn normalized(&self) -> f64 {
166        self.percentage()
167    }
168}
169
170// ============================================================================
171// Quality Issue
172// ============================================================================
173
174/// Severity of a quality issue
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176pub enum IssueSeverity {
177    /// Blocks release
178    Error,
179    /// Should be addressed
180    Warning,
181    /// Informational
182    Info,
183}
184
185/// A quality issue found during analysis
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct QualityIssue {
188    /// Issue type identifier
189    pub issue_type: String,
190    /// Human-readable message
191    pub message: String,
192    /// Severity level
193    pub severity: IssueSeverity,
194    /// Recommended action
195    pub recommendation: Option<String>,
196}
197
198impl QualityIssue {
199    /// Create a new quality issue
200    pub fn new(
201        issue_type: impl Into<String>,
202        message: impl Into<String>,
203        severity: IssueSeverity,
204    ) -> Self {
205        Self {
206            issue_type: issue_type.into(),
207            message: message.into(),
208            severity,
209            recommendation: None,
210        }
211    }
212
213    /// Add recommendation
214    pub fn with_recommendation(mut self, rec: impl Into<String>) -> Self {
215        self.recommendation = Some(rec.into());
216        self
217    }
218
219    /// Create error for score below threshold
220    pub fn score_below_threshold(metric: &str, score: u32, threshold: u32) -> Self {
221        Self::new(
222            format!("{}_below_threshold", metric),
223            format!("{} score {} below A- threshold ({})", metric, score, threshold),
224            IssueSeverity::Error,
225        )
226        .with_recommendation(format!("Improve {} to at least {}", metric, threshold))
227    }
228
229    /// Create error for missing hero image
230    pub fn missing_hero_image() -> Self {
231        Self::new("missing_hero_image", "No hero image found", IssueSeverity::Error)
232            .with_recommendation("Add hero.png to docs/ or include image at top of README.md")
233    }
234}
235
236// ============================================================================
237// Stack Layer
238// ============================================================================
239
240/// Layer in the PAIML stack architecture
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
242pub enum StackLayer {
243    /// Layer 0: Compute primitives (trueno, etc.)
244    Compute,
245    /// Layer 1: ML algorithms (aprender, etc.)
246    Ml,
247    /// Layer 2: Training & inference
248    Training,
249    /// Layer 3: Transpilers
250    Transpilers,
251    /// Layer 4: Orchestration
252    Orchestration,
253    /// Layer 5: Quality tools
254    Quality,
255    /// Layer 6: Data & MLOps
256    DataMlops,
257    /// Layer 7: Presentation
258    Presentation,
259}
260
261impl StackLayer {
262    /// Determine layer from component name
263    pub fn from_component(name: &str) -> Self {
264        match name {
265            "trueno" | "trueno-viz" | "trueno-db" | "trueno-graph" | "trueno-rag"
266            | "trueno-zram" => Self::Compute,
267            "aprender" | "aprender-shell" | "aprender-tsp" => Self::Ml,
268            "entrenar" | "realizar" | "whisper-apr" => Self::Training,
269            "depyler" | "decy" | "ruchy" => Self::Transpilers,
270            "batuta" | "repartir" | "pepita" | "pforge" | "forjar" => Self::Orchestration,
271            "certeza" | "renacer" | "pmat" | "provable-contracts" | "tiny-model-ground-truth" => {
272                Self::Quality
273            }
274            "alimentar" | "pacha" => Self::DataMlops,
275            "presentar"
276            | "sovereign-ai-stack-book"
277            | "apr-cookbook"
278            | "alm-cookbook"
279            | "pres-cookbook" => Self::Presentation,
280            _ => Self::Orchestration, // Default
281        }
282    }
283
284    /// Get display name for layer
285    pub fn display_name(&self) -> &'static str {
286        match self {
287            Self::Compute => "COMPUTE PRIMITIVES",
288            Self::Ml => "ML ALGORITHMS",
289            Self::Training => "TRAINING & INFERENCE",
290            Self::Transpilers => "TRANSPILERS",
291            Self::Orchestration => "ORCHESTRATION",
292            Self::Quality => "QUALITY",
293            Self::DataMlops => "DATA & MLOPS",
294            Self::Presentation => "PRESENTATION",
295        }
296    }
297}