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