ralph_workflow/guidelines/
base.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum CheckSeverity {
11 Critical,
13 High,
15 Medium,
17 Low,
19 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#[derive(Debug, Clone)]
37pub struct SeverityCheck {
38 pub(crate) check: String,
40 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#[derive(Debug, Clone)]
75pub struct ReviewGuidelines {
76 pub(crate) quality_checks: Vec<String>,
78 pub(crate) security_checks: Vec<String>,
80 pub(crate) performance_checks: Vec<String>,
82 pub(crate) testing_checks: Vec<String>,
84 pub(crate) documentation_checks: Vec<String>,
86 pub(crate) idioms: Vec<String>,
88 pub(crate) anti_patterns: Vec<String>,
90 pub(crate) concurrency_checks: Vec<String>,
92 pub(crate) resource_checks: Vec<String>,
94 pub(crate) observability_checks: Vec<String>,
96 pub(crate) secrets_checks: Vec<String>,
98 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 pub(crate) fn get_all_checks(&self) -> Vec<SeverityCheck> {
160 let mut checks = Vec::new();
161
162 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 for check in &self.concurrency_checks {
172 checks.push(SeverityCheck::high(check.clone()));
173 }
174
175 for check in &self.resource_checks {
177 checks.push(SeverityCheck::high(check.clone()));
178 }
179
180 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 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 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 for check in &self.idioms {
209 checks.push(SeverityCheck::info(check.clone()));
210 }
211
212 checks
213 }
214
215 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 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#[cfg(test)]
245impl ReviewGuidelines {
246 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 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 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 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 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 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 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 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 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 assert!(!all_checks.is_empty());
432
433 let critical_count = all_checks
435 .iter()
436 .filter(|c| c.severity == CheckSeverity::Critical)
437 .count();
438 assert!(critical_count > 0);
439
440 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 assert!(formatted.contains("CRITICAL"));
455 assert!(formatted.contains("HIGH"));
456 assert!(formatted.contains("MEDIUM"));
457 assert!(formatted.contains("LOW"));
458
459 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 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); }
496
497 #[test]
498 fn test_default_has_new_check_categories() {
499 let guidelines = ReviewGuidelines::default();
500
501 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}