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)]
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 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 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 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#[cfg(test)]
209impl ReviewGuidelines {
210 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 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 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 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 assert!(!all_checks.is_empty());
374
375 let critical_count = all_checks
377 .iter()
378 .filter(|c| c.severity == CheckSeverity::Critical)
379 .count();
380 assert!(critical_count > 0);
381
382 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 assert!(formatted.contains("CRITICAL"));
397 assert!(formatted.contains("HIGH"));
398 assert!(formatted.contains("MEDIUM"));
399 assert!(formatted.contains("LOW"));
400
401 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 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); }
438
439 #[test]
440 fn test_default_has_new_check_categories() {
441 let guidelines = ReviewGuidelines::default();
442
443 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}