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