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        self.security_checks
151            .iter()
152            .chain(self.secrets_checks.iter())
153            .map(SeverityCheck::critical)
154            .chain(
155                self.concurrency_checks
156                    .iter()
157                    .chain(self.resource_checks.iter())
158                    .map(SeverityCheck::high),
159            )
160            .chain(
161                self.quality_checks
162                    .iter()
163                    .chain(self.anti_patterns.iter())
164                    .chain(self.performance_checks.iter())
165                    .chain(self.testing_checks.iter())
166                    .chain(self.api_design_checks.iter())
167                    .map(SeverityCheck::medium),
168            )
169            .chain(
170                self.observability_checks
171                    .iter()
172                    .chain(self.documentation_checks.iter())
173                    .map(SeverityCheck::low),
174            )
175            .chain(self.idioms.iter().map(SeverityCheck::info))
176            .collect()
177    }
178
179    /// Get a brief summary for display
180    pub(crate) fn summary(&self) -> String {
181        format!(
182            "{} quality checks, {} security checks, {} anti-patterns",
183            self.quality_checks.len(),
184            self.security_checks.len(),
185            self.anti_patterns.len()
186        )
187    }
188
189    /// Get a comprehensive count of all checks
190    pub(crate) const fn total_checks(&self) -> usize {
191        self.quality_checks.len()
192            + self.security_checks.len()
193            + self.performance_checks.len()
194            + self.testing_checks.len()
195            + self.documentation_checks.len()
196            + self.idioms.len()
197            + self.anti_patterns.len()
198            + self.concurrency_checks.len()
199            + self.resource_checks.len()
200            + self.observability_checks.len()
201            + self.secrets_checks.len()
202            + self.api_design_checks.len()
203    }
204}
205
206/// Test-only methods for `ReviewGuidelines`.
207/// These are used by tests to format guidelines into prompts.
208#[cfg(test)]
209impl ReviewGuidelines {
210    /// Format a section of guidelines with a title and item limit.
211    fn format_section(items: &[String], title: &str, limit: usize) -> Option<String> {
212        if items.is_empty() {
213            return None;
214        }
215        let lines: Vec<String> = items
216            .iter()
217            .take(limit)
218            .map(|s| format!("  - {s}"))
219            .chain(if items.len() > limit {
220                Some(format!("  - ... (+{} more)", items.len() - limit))
221            } else {
222                None
223            })
224            .collect();
225        Some(format!("{}:\n{}", title, lines.join("\n")))
226    }
227
228    /// Format guidelines as a prompt section
229    pub(crate) fn format_for_prompt(&self) -> String {
230        let sections: Vec<String> = [
231            Self::format_section(&self.quality_checks, "CODE QUALITY", 10),
232            Self::format_section(&self.security_checks, "SECURITY", 10),
233            Self::format_section(&self.performance_checks, "PERFORMANCE", 8),
234            Self::format_section(&self.anti_patterns, "AVOID", 8),
235        ]
236        .into_iter()
237        .flatten()
238        .collect();
239
240        sections.join("\n\n")
241    }
242
243    /// Format guidelines with severity priorities for the review prompt.
244    ///
245    /// This produces a more detailed prompt section that groups checks by priority,
246    /// helping agents focus on the most critical issues first.
247    pub(crate) fn format_for_prompt_with_priorities(&self) -> String {
248        fn build_section(header: &str, checks: &[SeverityCheck], limit: usize) -> Option<String> {
249            if checks.is_empty() {
250                return None;
251            }
252            let items: Vec<String> = checks
253                .iter()
254                .take(limit)
255                .map(|c| format!("  - {}", c.check))
256                .chain(if checks.len() > limit {
257                    Some(format!("  - ... (+{} more)", checks.len() - limit))
258                } else {
259                    None
260                })
261                .collect();
262            Some(format!("{}\n{}", header, items.join("\n")))
263        }
264
265        let critical_checks: Vec<SeverityCheck> = self
266            .security_checks
267            .iter()
268            .chain(self.secrets_checks.iter())
269            .cloned()
270            .map(SeverityCheck::critical)
271            .collect();
272
273        let high_checks: Vec<SeverityCheck> = self
274            .concurrency_checks
275            .iter()
276            .chain(self.resource_checks.iter())
277            .cloned()
278            .map(SeverityCheck::high)
279            .collect();
280
281        let medium_checks: Vec<SeverityCheck> = self
282            .quality_checks
283            .iter()
284            .chain(self.anti_patterns.iter())
285            .chain(self.performance_checks.iter())
286            .chain(self.testing_checks.iter())
287            .chain(self.api_design_checks.iter())
288            .cloned()
289            .map(SeverityCheck::medium)
290            .collect();
291
292        let low_checks: Vec<SeverityCheck> = self
293            .documentation_checks
294            .iter()
295            .chain(self.observability_checks.iter())
296            .cloned()
297            .map(SeverityCheck::low)
298            .collect();
299
300        let info_checks: Vec<SeverityCheck> = self
301            .idioms
302            .iter()
303            .cloned()
304            .map(SeverityCheck::info)
305            .collect();
306
307        let sections: Vec<String> = [
308            build_section("CRITICAL (must fix before merge):", &critical_checks, 10),
309            build_section("HIGH (should fix before merge):", &high_checks, 10),
310            build_section("MEDIUM (should address):", &medium_checks, 12),
311            build_section("LOW (nice to have):", &low_checks, 10),
312            build_section("INFO (observations):", &info_checks, 10),
313        ]
314        .into_iter()
315        .flatten()
316        .collect();
317
318        sections.join("\n\n")
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_default_guidelines() {
328        let guidelines = ReviewGuidelines::default();
329        assert!(!guidelines.quality_checks.is_empty());
330        assert!(!guidelines.security_checks.is_empty());
331    }
332
333    #[test]
334    fn test_check_severity_ordering() {
335        // Critical should be less than (higher priority) than High, etc.
336        assert!(CheckSeverity::Critical < CheckSeverity::High);
337        assert!(CheckSeverity::High < CheckSeverity::Medium);
338        assert!(CheckSeverity::Medium < CheckSeverity::Low);
339        assert!(CheckSeverity::Low < CheckSeverity::Info);
340    }
341
342    #[test]
343    fn test_check_severity_display() {
344        assert_eq!(format!("{}", CheckSeverity::Critical), "CRITICAL");
345        assert_eq!(format!("{}", CheckSeverity::High), "HIGH");
346        assert_eq!(format!("{}", CheckSeverity::Medium), "MEDIUM");
347        assert_eq!(format!("{}", CheckSeverity::Low), "LOW");
348        assert_eq!(format!("{}", CheckSeverity::Info), "INFO");
349    }
350
351    #[test]
352    fn test_severity_check_constructors() {
353        let critical = SeverityCheck::critical("test");
354        assert_eq!(critical.severity, CheckSeverity::Critical);
355        assert_eq!(critical.check, "test");
356
357        let high = SeverityCheck::high("high test");
358        assert_eq!(high.severity, CheckSeverity::High);
359
360        let medium = SeverityCheck::medium("medium test");
361        assert_eq!(medium.severity, CheckSeverity::Medium);
362
363        let low = SeverityCheck::low("low test");
364        assert_eq!(low.severity, CheckSeverity::Low);
365    }
366
367    #[test]
368    fn test_get_all_checks() {
369        let guidelines = ReviewGuidelines::default();
370        let all_checks = guidelines.get_all_checks();
371
372        // Should have checks from all categories
373        assert!(!all_checks.is_empty());
374
375        // Security checks should be critical
376        let critical_count = all_checks
377            .iter()
378            .filter(|c| c.severity == CheckSeverity::Critical)
379            .count();
380        assert!(critical_count > 0);
381
382        // Should have some medium severity checks
383        let medium_count = all_checks
384            .iter()
385            .filter(|c| c.severity == CheckSeverity::Medium)
386            .count();
387        assert!(medium_count > 0);
388    }
389
390    #[test]
391    fn test_format_for_prompt_with_priorities() {
392        let guidelines = ReviewGuidelines::default();
393        let formatted = guidelines.format_for_prompt_with_priorities();
394
395        // Should contain priority indicators
396        assert!(formatted.contains("CRITICAL"));
397        assert!(formatted.contains("HIGH"));
398        assert!(formatted.contains("MEDIUM"));
399        assert!(formatted.contains("LOW"));
400
401        // Should not omit new/extended categories
402        assert!(formatted.contains("API follows consistent naming conventions"));
403        assert!(formatted.contains("Code follows language conventions"));
404    }
405
406    #[test]
407    fn test_summary() {
408        let guidelines = ReviewGuidelines::default();
409        let summary = guidelines.summary();
410
411        assert!(summary.contains("quality checks"));
412        assert!(summary.contains("security checks"));
413        assert!(summary.contains("anti-patterns"));
414    }
415
416    #[test]
417    fn test_total_checks() {
418        let guidelines = ReviewGuidelines::default();
419        let total = guidelines.total_checks();
420
421        // Should be the sum of all check categories
422        let expected = guidelines.quality_checks.len()
423            + guidelines.security_checks.len()
424            + guidelines.performance_checks.len()
425            + guidelines.testing_checks.len()
426            + guidelines.documentation_checks.len()
427            + guidelines.idioms.len()
428            + guidelines.anti_patterns.len()
429            + guidelines.concurrency_checks.len()
430            + guidelines.resource_checks.len()
431            + guidelines.observability_checks.len()
432            + guidelines.secrets_checks.len()
433            + guidelines.api_design_checks.len();
434
435        assert_eq!(total, expected);
436        assert!(total > 10); // Should have a reasonable number of checks
437    }
438
439    #[test]
440    fn test_default_has_new_check_categories() {
441        let guidelines = ReviewGuidelines::default();
442
443        // New categories should have defaults
444        assert!(!guidelines.concurrency_checks.is_empty());
445        assert!(!guidelines.resource_checks.is_empty());
446        assert!(!guidelines.observability_checks.is_empty());
447        assert!(!guidelines.secrets_checks.is_empty());
448        assert!(!guidelines.api_design_checks.is_empty());
449    }
450}