Skip to main content

ralph_workflow/guidelines/
base.rs

1//! Core types for review guidelines
2//!
3//! Contains the fundamental structures used across all language-specific guideline modules.
4
5/// Severity level for code review checks
6///
7/// Used to prioritize review feedback and help developers focus on
8/// the most important issues first.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum CheckSeverity {
11    /// Must fix before merge - security vulnerabilities, data loss, crashes
12    Critical,
13    /// Should fix before merge - bugs, significant functional issues
14    High,
15    /// Should address - code quality, maintainability concerns
16    Medium,
17    /// Nice to have - minor improvements, style suggestions
18    Low,
19    /// Informational - observations, suggestions for future consideration
20    Info,
21}
22
23impl std::fmt::Display for CheckSeverity {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Critical => write!(f, "CRITICAL"),
27            Self::High => write!(f, "HIGH"),
28            Self::Medium => write!(f, "MEDIUM"),
29            Self::Low => write!(f, "LOW"),
30            Self::Info => write!(f, "INFO"),
31        }
32    }
33}
34
35/// A review check with associated severity
36#[derive(Debug, Clone)]
37pub struct SeverityCheck {
38    /// The check description
39    pub(crate) check: String,
40    /// Severity level for this check
41    pub(crate) severity: CheckSeverity,
42}
43
44impl SeverityCheck {
45    pub(crate) fn new(check: impl Into<String>, severity: CheckSeverity) -> Self {
46        Self {
47            check: check.into(),
48            severity,
49        }
50    }
51
52    pub(crate) fn critical(check: impl Into<String>) -> Self {
53        Self::new(check, CheckSeverity::Critical)
54    }
55
56    pub(crate) fn high(check: impl Into<String>) -> Self {
57        Self::new(check, CheckSeverity::High)
58    }
59
60    pub(crate) fn medium(check: impl Into<String>) -> Self {
61        Self::new(check, CheckSeverity::Medium)
62    }
63
64    pub(crate) fn low(check: impl Into<String>) -> Self {
65        Self::new(check, CheckSeverity::Low)
66    }
67
68    pub(crate) fn info(check: impl Into<String>) -> Self {
69        Self::new(check, CheckSeverity::Info)
70    }
71}
72
73/// Review guidelines for a specific technology stack.
74///
75/// Each field is a category of checks/expectations used to generate review guidance.
76#[derive(Debug, Clone)]
77pub struct ReviewGuidelines {
78    pub(crate) quality_checks: Vec<String>,
79    pub(crate) security_checks: Vec<String>,
80    pub(crate) performance_checks: Vec<String>,
81    pub(crate) testing_checks: Vec<String>,
82    pub(crate) documentation_checks: Vec<String>,
83    pub(crate) idioms: Vec<String>,
84    pub(crate) anti_patterns: Vec<String>,
85    pub(crate) concurrency_checks: Vec<String>,
86    pub(crate) resource_checks: Vec<String>,
87    pub(crate) observability_checks: Vec<String>,
88    pub(crate) secrets_checks: Vec<String>,
89    pub(crate) api_design_checks: Vec<String>,
90}
91
92impl Default for ReviewGuidelines {
93    fn default() -> Self {
94        Self {
95            quality_checks: vec![
96                "Code follows consistent style and formatting".to_string(),
97                "Functions have single responsibility".to_string(),
98                "Error handling is comprehensive".to_string(),
99                "No dead code or unused imports".to_string(),
100            ],
101            security_checks: vec![
102                "No hardcoded secrets or credentials".to_string(),
103                "Input validation on external data".to_string(),
104                "Proper authentication/authorization checks".to_string(),
105            ],
106            performance_checks: vec![
107                "No obvious performance bottlenecks".to_string(),
108                "Efficient data structures used".to_string(),
109            ],
110            testing_checks: vec![
111                "Tests cover main functionality".to_string(),
112                "Edge cases are tested".to_string(),
113            ],
114            documentation_checks: vec![
115                "Public APIs are documented".to_string(),
116                "Complex logic has explanatory comments".to_string(),
117            ],
118            idioms: vec!["Code follows language conventions".to_string()],
119            anti_patterns: vec!["Avoid code duplication".to_string()],
120            concurrency_checks: vec![
121                "Shared mutable state is properly synchronized".to_string(),
122                "No potential deadlocks (lock ordering)".to_string(),
123            ],
124            resource_checks: vec![
125                "Resources are properly closed/released".to_string(),
126                "No resource leaks in error paths".to_string(),
127            ],
128            observability_checks: vec![
129                "Errors are logged with context".to_string(),
130                "Critical operations have appropriate logging".to_string(),
131            ],
132            secrets_checks: vec![
133                "Secrets loaded from environment/config, not hardcoded".to_string(),
134                "Sensitive data not logged or exposed in errors".to_string(),
135            ],
136            api_design_checks: vec![
137                "API follows consistent naming conventions".to_string(),
138                "Breaking changes are clearly documented".to_string(),
139            ],
140        }
141    }
142}
143
144impl ReviewGuidelines {
145    /// Get all checks with their severity classifications
146    ///
147    /// Returns a comprehensive list of all applicable checks organized by category
148    /// with severity levels. This is useful for generating detailed review reports.
149    pub(crate) fn get_all_checks(&self) -> Vec<SeverityCheck> {
150        let mut checks = Vec::new();
151
152        // Security checks are CRITICAL
153        for check in &self.security_checks {
154            checks.push(SeverityCheck::critical(check.clone()));
155        }
156        for check in &self.secrets_checks {
157            checks.push(SeverityCheck::critical(check.clone()));
158        }
159
160        // Concurrency issues are HIGH severity
161        for check in &self.concurrency_checks {
162            checks.push(SeverityCheck::high(check.clone()));
163        }
164
165        // Concurrency and resource management issues are HIGH
166        for check in &self.resource_checks {
167            checks.push(SeverityCheck::high(check.clone()));
168        }
169
170        // Quality issues are MEDIUM
171        for check in &self.quality_checks {
172            checks.push(SeverityCheck::medium(check.clone()));
173        }
174        for check in &self.anti_patterns {
175            checks.push(SeverityCheck::medium(check.clone()));
176        }
177
178        // Performance, testing, API design are MEDIUM
179        for check in &self.performance_checks {
180            checks.push(SeverityCheck::medium(check.clone()));
181        }
182        for check in &self.testing_checks {
183            checks.push(SeverityCheck::medium(check.clone()));
184        }
185        for check in &self.api_design_checks {
186            checks.push(SeverityCheck::medium(check.clone()));
187        }
188
189        // Observability and documentation are LOW
190        for check in &self.observability_checks {
191            checks.push(SeverityCheck::low(check.clone()));
192        }
193        for check in &self.documentation_checks {
194            checks.push(SeverityCheck::low(check.clone()));
195        }
196
197        // Idioms are informational.
198        for check in &self.idioms {
199            checks.push(SeverityCheck::info(check.clone()));
200        }
201
202        checks
203    }
204
205    /// Get a brief summary for display
206    pub(crate) fn summary(&self) -> String {
207        format!(
208            "{} quality checks, {} security checks, {} anti-patterns",
209            self.quality_checks.len(),
210            self.security_checks.len(),
211            self.anti_patterns.len()
212        )
213    }
214
215    /// Get a comprehensive count of all checks
216    pub(crate) const fn total_checks(&self) -> usize {
217        self.quality_checks.len()
218            + self.security_checks.len()
219            + self.performance_checks.len()
220            + self.testing_checks.len()
221            + self.documentation_checks.len()
222            + self.idioms.len()
223            + self.anti_patterns.len()
224            + self.concurrency_checks.len()
225            + self.resource_checks.len()
226            + self.observability_checks.len()
227            + self.secrets_checks.len()
228            + self.api_design_checks.len()
229    }
230}
231
232/// Test-only methods for ReviewGuidelines.
233/// These are used by tests to format guidelines into prompts.
234#[cfg(test)]
235impl ReviewGuidelines {
236    /// Format a section of guidelines with a title and item limit.
237    fn format_section(items: &[String], title: &str, limit: usize) -> Option<String> {
238        if items.is_empty() {
239            return None;
240        }
241        let mut lines: Vec<String> = items
242            .iter()
243            .take(limit)
244            .map(|s| format!("  - {s}"))
245            .collect();
246        if items.len() > limit {
247            lines.push(format!("  - ... (+{} more)", items.len() - limit));
248        }
249        Some(format!("{}:\n{}", title, lines.join("\n")))
250    }
251
252    /// Format guidelines as a prompt section
253    pub(crate) fn format_for_prompt(&self) -> String {
254        let mut sections = Vec::new();
255
256        if let Some(s) = Self::format_section(&self.quality_checks, "CODE QUALITY", 10) {
257            sections.push(s);
258        }
259        if let Some(s) = Self::format_section(&self.security_checks, "SECURITY", 10) {
260            sections.push(s);
261        }
262        if let Some(s) = Self::format_section(&self.performance_checks, "PERFORMANCE", 8) {
263            sections.push(s);
264        }
265        if let Some(s) = Self::format_section(&self.anti_patterns, "AVOID", 8) {
266            sections.push(s);
267        }
268
269        sections.join("\n\n")
270    }
271
272    /// Format guidelines with severity priorities for the review prompt.
273    ///
274    /// This produces a more detailed prompt section that groups checks by priority,
275    /// helping agents focus on the most critical issues first.
276    pub(crate) fn format_for_prompt_with_priorities(&self) -> String {
277        fn push_section(
278            sections: &mut Vec<String>,
279            header: &str,
280            checks: &[SeverityCheck],
281            limit: usize,
282        ) {
283            if checks.is_empty() {
284                return;
285            }
286            let mut items: Vec<String> = checks
287                .iter()
288                .take(limit)
289                .map(|c| format!("  - {}", c.check))
290                .collect();
291            if checks.len() > limit {
292                items.push(format!("  - ... (+{} more)", checks.len() - limit));
293            }
294            sections.push(format!("{}\n{}", header, items.join("\n")));
295        }
296
297        let mut sections = Vec::new();
298
299        // Critical: Security and secrets.
300        let critical_checks: Vec<SeverityCheck> = self
301            .security_checks
302            .iter()
303            .chain(self.secrets_checks.iter())
304            .cloned()
305            .map(SeverityCheck::critical)
306            .collect();
307        push_section(
308            &mut sections,
309            "CRITICAL (must fix before merge):",
310            &critical_checks,
311            10,
312        );
313
314        // High: Concurrency and resource management.
315        let high_checks: Vec<SeverityCheck> = self
316            .concurrency_checks
317            .iter()
318            .chain(self.resource_checks.iter())
319            .cloned()
320            .map(SeverityCheck::high)
321            .collect();
322        push_section(
323            &mut sections,
324            "HIGH (should fix before merge):",
325            &high_checks,
326            10,
327        );
328
329        // Medium: Quality, anti-patterns, performance, testing, API design.
330        let medium_checks: Vec<SeverityCheck> = self
331            .quality_checks
332            .iter()
333            .chain(self.anti_patterns.iter())
334            .chain(self.performance_checks.iter())
335            .chain(self.testing_checks.iter())
336            .chain(self.api_design_checks.iter())
337            .cloned()
338            .map(SeverityCheck::medium)
339            .collect();
340        push_section(
341            &mut sections,
342            "MEDIUM (should address):",
343            &medium_checks,
344            12,
345        );
346
347        // Low: Documentation, observability.
348        let low_checks: Vec<SeverityCheck> = self
349            .documentation_checks
350            .iter()
351            .chain(self.observability_checks.iter())
352            .cloned()
353            .map(SeverityCheck::low)
354            .collect();
355        push_section(&mut sections, "LOW (nice to have):", &low_checks, 10);
356
357        // Info: Idioms.
358        let info_checks: Vec<SeverityCheck> = self
359            .idioms
360            .iter()
361            .cloned()
362            .map(SeverityCheck::info)
363            .collect();
364        push_section(&mut sections, "INFO (observations):", &info_checks, 10);
365
366        sections.join("\n\n")
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_default_guidelines() {
376        let guidelines = ReviewGuidelines::default();
377        assert!(!guidelines.quality_checks.is_empty());
378        assert!(!guidelines.security_checks.is_empty());
379    }
380
381    #[test]
382    fn test_check_severity_ordering() {
383        // Critical should be less than (higher priority) than High, etc.
384        assert!(CheckSeverity::Critical < CheckSeverity::High);
385        assert!(CheckSeverity::High < CheckSeverity::Medium);
386        assert!(CheckSeverity::Medium < CheckSeverity::Low);
387        assert!(CheckSeverity::Low < CheckSeverity::Info);
388    }
389
390    #[test]
391    fn test_check_severity_display() {
392        assert_eq!(format!("{}", CheckSeverity::Critical), "CRITICAL");
393        assert_eq!(format!("{}", CheckSeverity::High), "HIGH");
394        assert_eq!(format!("{}", CheckSeverity::Medium), "MEDIUM");
395        assert_eq!(format!("{}", CheckSeverity::Low), "LOW");
396        assert_eq!(format!("{}", CheckSeverity::Info), "INFO");
397    }
398
399    #[test]
400    fn test_severity_check_constructors() {
401        let critical = SeverityCheck::critical("test");
402        assert_eq!(critical.severity, CheckSeverity::Critical);
403        assert_eq!(critical.check, "test");
404
405        let high = SeverityCheck::high("high test");
406        assert_eq!(high.severity, CheckSeverity::High);
407
408        let medium = SeverityCheck::medium("medium test");
409        assert_eq!(medium.severity, CheckSeverity::Medium);
410
411        let low = SeverityCheck::low("low test");
412        assert_eq!(low.severity, CheckSeverity::Low);
413    }
414
415    #[test]
416    fn test_get_all_checks() {
417        let guidelines = ReviewGuidelines::default();
418        let all_checks = guidelines.get_all_checks();
419
420        // Should have checks from all categories
421        assert!(!all_checks.is_empty());
422
423        // Security checks should be critical
424        let critical_count = all_checks
425            .iter()
426            .filter(|c| c.severity == CheckSeverity::Critical)
427            .count();
428        assert!(critical_count > 0);
429
430        // Should have some medium severity checks
431        let medium_count = all_checks
432            .iter()
433            .filter(|c| c.severity == CheckSeverity::Medium)
434            .count();
435        assert!(medium_count > 0);
436    }
437
438    #[test]
439    fn test_format_for_prompt_with_priorities() {
440        let guidelines = ReviewGuidelines::default();
441        let formatted = guidelines.format_for_prompt_with_priorities();
442
443        // Should contain priority indicators
444        assert!(formatted.contains("CRITICAL"));
445        assert!(formatted.contains("HIGH"));
446        assert!(formatted.contains("MEDIUM"));
447        assert!(formatted.contains("LOW"));
448
449        // Should not omit new/extended categories
450        assert!(formatted.contains("API follows consistent naming conventions"));
451        assert!(formatted.contains("Code follows language conventions"));
452    }
453
454    #[test]
455    fn test_summary() {
456        let guidelines = ReviewGuidelines::default();
457        let summary = guidelines.summary();
458
459        assert!(summary.contains("quality checks"));
460        assert!(summary.contains("security checks"));
461        assert!(summary.contains("anti-patterns"));
462    }
463
464    #[test]
465    fn test_total_checks() {
466        let guidelines = ReviewGuidelines::default();
467        let total = guidelines.total_checks();
468
469        // Should be the sum of all check categories
470        let expected = guidelines.quality_checks.len()
471            + guidelines.security_checks.len()
472            + guidelines.performance_checks.len()
473            + guidelines.testing_checks.len()
474            + guidelines.documentation_checks.len()
475            + guidelines.idioms.len()
476            + guidelines.anti_patterns.len()
477            + guidelines.concurrency_checks.len()
478            + guidelines.resource_checks.len()
479            + guidelines.observability_checks.len()
480            + guidelines.secrets_checks.len()
481            + guidelines.api_design_checks.len();
482
483        assert_eq!(total, expected);
484        assert!(total > 10); // Should have a reasonable number of checks
485    }
486
487    #[test]
488    fn test_default_has_new_check_categories() {
489        let guidelines = ReviewGuidelines::default();
490
491        // New categories should have defaults
492        assert!(!guidelines.concurrency_checks.is_empty());
493        assert!(!guidelines.resource_checks.is_empty());
494        assert!(!guidelines.observability_checks.is_empty());
495        assert!(!guidelines.secrets_checks.is_empty());
496        assert!(!guidelines.api_design_checks.is_empty());
497    }
498}