Skip to main content

ruvllm/reflection/
perspectives.rs

1//! Multi-Perspective Critique System
2//!
3//! Implements a multi-perspective critique system that evaluates outputs from
4//! different angles to provide comprehensive reflection. Each perspective focuses
5//! on a specific quality dimension.
6//!
7//! ## Available Perspectives
8//!
9//! - **Correctness**: Verifies logical correctness and absence of errors
10//! - **Completeness**: Checks if all requirements are addressed
11//! - **Consistency**: Ensures internal consistency and follows conventions
12//!
13//! ## Architecture
14//!
15//! ```text
16//! +-------------------+     +----------------------+
17//! | Perspective trait |<----| CorrectnessChecker   |
18//! | - critique()      |     +----------------------+
19//! | - name()          |<----| CompletenessChecker  |
20//! +-------------------+     +----------------------+
21//!                      <----| ConsistencyChecker   |
22//!                           +----------------------+
23//!           |
24//!           v
25//! +-------------------+     +----------------------+
26//! | CritiqueResult    |---->| UnifiedCritique      |
27//! | - passed          |     | - combine results    |
28//! | - score           |     | - generate summary   |
29//! | - issues          |     +----------------------+
30//! +-------------------+
31//! ```
32
33use super::reflective_agent::ExecutionContext;
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36
37/// Configuration for perspectives
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PerspectiveConfig {
40    /// Weight for this perspective in combined scoring
41    pub weight: f32,
42    /// Minimum score to pass
43    pub pass_threshold: f32,
44    /// Whether to provide detailed feedback
45    pub detailed_feedback: bool,
46    /// Custom checks to perform
47    pub custom_checks: Vec<String>,
48}
49
50impl Default for PerspectiveConfig {
51    fn default() -> Self {
52        Self {
53            weight: 1.0,
54            pass_threshold: 0.6,
55            detailed_feedback: true,
56            custom_checks: Vec::new(),
57        }
58    }
59}
60
61/// Result of a critique from one perspective
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CritiqueResult {
64    /// Name of the perspective
65    pub perspective_name: String,
66    /// Whether the critique passed
67    pub passed: bool,
68    /// Score (0.0-1.0)
69    pub score: f32,
70    /// Summary of the critique
71    pub summary: String,
72    /// Specific issues found
73    pub issues: Vec<CritiqueIssue>,
74    /// Strengths identified
75    pub strengths: Vec<String>,
76    /// Time taken for critique (ms)
77    pub critique_time_ms: u64,
78}
79
80impl CritiqueResult {
81    /// Create a new passing critique result
82    pub fn pass(perspective: impl Into<String>, score: f32, summary: impl Into<String>) -> Self {
83        Self {
84            perspective_name: perspective.into(),
85            passed: true,
86            score: score.clamp(0.0, 1.0),
87            summary: summary.into(),
88            issues: Vec::new(),
89            strengths: Vec::new(),
90            critique_time_ms: 0,
91        }
92    }
93
94    /// Create a failing critique result
95    pub fn fail(perspective: impl Into<String>, score: f32, summary: impl Into<String>) -> Self {
96        Self {
97            perspective_name: perspective.into(),
98            passed: false,
99            score: score.clamp(0.0, 1.0),
100            summary: summary.into(),
101            issues: Vec::new(),
102            strengths: Vec::new(),
103            critique_time_ms: 0,
104        }
105    }
106
107    /// Add an issue
108    pub fn with_issue(mut self, issue: CritiqueIssue) -> Self {
109        self.issues.push(issue);
110        self
111    }
112
113    /// Add a strength
114    pub fn with_strength(mut self, strength: impl Into<String>) -> Self {
115        self.strengths.push(strength.into());
116        self
117    }
118}
119
120/// A specific issue found during critique
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CritiqueIssue {
123    /// Issue severity (0.0-1.0)
124    pub severity: f32,
125    /// Issue description
126    pub description: String,
127    /// Location (line number or section)
128    pub location: Option<String>,
129    /// Suggested fix
130    pub suggestion: String,
131    /// Category of issue
132    pub category: IssueCategory,
133}
134
135impl CritiqueIssue {
136    /// Create a new critique issue
137    pub fn new(description: impl Into<String>, severity: f32, category: IssueCategory) -> Self {
138        Self {
139            severity: severity.clamp(0.0, 1.0),
140            description: description.into(),
141            location: None,
142            suggestion: String::new(),
143            category,
144        }
145    }
146
147    /// Add location
148    pub fn at(mut self, location: impl Into<String>) -> Self {
149        self.location = Some(location.into());
150        self
151    }
152
153    /// Add suggestion
154    pub fn suggest(mut self, suggestion: impl Into<String>) -> Self {
155        self.suggestion = suggestion.into();
156        self
157    }
158}
159
160/// Categories of critique issues
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
162pub enum IssueCategory {
163    /// Logical error
164    Logic,
165    /// Syntax or structural issue
166    Syntax,
167    /// Missing element
168    Missing,
169    /// Redundant element
170    Redundant,
171    /// Inconsistency
172    Inconsistent,
173    /// Style or convention violation
174    Style,
175    /// Security concern
176    Security,
177    /// Performance concern
178    Performance,
179    /// Documentation gap
180    Documentation,
181    /// Other
182    Other,
183}
184
185/// Trait for perspective implementations
186pub trait Perspective: Send + Sync {
187    /// Get the perspective name
188    fn name(&self) -> &str;
189
190    /// Perform critique from this perspective
191    fn critique(&self, output: &str, context: &ExecutionContext) -> CritiqueResult;
192
193    /// Get the configuration
194    fn config(&self) -> &PerspectiveConfig;
195}
196
197/// Correctness checker perspective
198///
199/// Verifies logical correctness, absence of errors, and proper functioning
200pub struct CorrectnessChecker {
201    config: PerspectiveConfig,
202}
203
204impl CorrectnessChecker {
205    /// Create a new correctness checker
206    pub fn new() -> Self {
207        Self {
208            config: PerspectiveConfig {
209                weight: 1.2, // Higher weight for correctness
210                pass_threshold: 0.7,
211                detailed_feedback: true,
212                custom_checks: Vec::new(),
213            },
214        }
215    }
216
217    /// Create with custom config
218    pub fn with_config(config: PerspectiveConfig) -> Self {
219        Self { config }
220    }
221
222    /// Check for error patterns in output
223    fn check_for_errors(&self, output: &str) -> Vec<CritiqueIssue> {
224        let mut issues = Vec::new();
225
226        // Check for explicit error markers
227        let error_patterns = [
228            ("error[", "Compiler error present", IssueCategory::Syntax),
229            ("Error:", "Runtime error present", IssueCategory::Logic),
230            ("panic!", "Panic in code", IssueCategory::Logic),
231            (
232                "unwrap()",
233                "Potential panic from unwrap",
234                IssueCategory::Logic,
235            ),
236            (
237                "expect()",
238                "Potential panic from expect",
239                IssueCategory::Logic,
240            ),
241            ("todo!()", "Unimplemented todo", IssueCategory::Missing),
242            (
243                "unimplemented!()",
244                "Unimplemented code",
245                IssueCategory::Missing,
246            ),
247            (
248                "unreachable!()",
249                "Unreachable code marker",
250                IssueCategory::Logic,
251            ),
252        ];
253
254        for (pattern, description, category) in error_patterns {
255            if output.contains(pattern) {
256                let count = output.matches(pattern).count();
257                issues.push(
258                    CritiqueIssue::new(
259                        format!("{} ({} occurrence(s))", description, count),
260                        if category == IssueCategory::Logic {
261                            0.8
262                        } else {
263                            0.5
264                        },
265                        category,
266                    )
267                    .suggest(format!("Address or remove {}", pattern)),
268                );
269            }
270        }
271
272        // Check for unbalanced brackets (potential syntax errors)
273        let open_parens = output.matches('(').count();
274        let close_parens = output.matches(')').count();
275        if open_parens != close_parens {
276            issues.push(
277                CritiqueIssue::new(
278                    format!(
279                        "Unbalanced parentheses: {} open, {} close",
280                        open_parens, close_parens
281                    ),
282                    0.7,
283                    IssueCategory::Syntax,
284                )
285                .suggest("Check for missing or extra parentheses"),
286            );
287        }
288
289        let open_braces = output.matches('{').count();
290        let close_braces = output.matches('}').count();
291        if open_braces != close_braces {
292            issues.push(
293                CritiqueIssue::new(
294                    format!(
295                        "Unbalanced braces: {} open, {} close",
296                        open_braces, close_braces
297                    ),
298                    0.7,
299                    IssueCategory::Syntax,
300                )
301                .suggest("Check for missing or extra braces"),
302            );
303        }
304
305        issues
306    }
307
308    /// Check for logic issues
309    fn check_logic(&self, output: &str) -> Vec<CritiqueIssue> {
310        let mut issues = Vec::new();
311
312        // Check for potential infinite loops
313        if output.contains("loop {") && !output.contains("break") {
314            issues.push(
315                CritiqueIssue::new(
316                    "Potential infinite loop without break",
317                    0.6,
318                    IssueCategory::Logic,
319                )
320                .suggest("Add break condition or use while/for loop"),
321            );
322        }
323
324        // Check for empty functions
325        let empty_fn_pattern = "fn ";
326        if output.contains(empty_fn_pattern) {
327            // Simple heuristic: function with just {}
328            if output.contains("{ }") || output.contains("{}") {
329                issues.push(
330                    CritiqueIssue::new("Empty function body detected", 0.4, IssueCategory::Missing)
331                        .suggest("Implement function body or add todo!()"),
332                );
333            }
334        }
335
336        // Check for hardcoded values that might be problematic
337        if output.contains("localhost") || output.contains("127.0.0.1") {
338            issues.push(
339                CritiqueIssue::new("Hardcoded localhost/IP address", 0.3, IssueCategory::Style)
340                    .suggest("Consider using configuration or environment variables"),
341            );
342        }
343
344        issues
345    }
346}
347
348impl Default for CorrectnessChecker {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354impl Perspective for CorrectnessChecker {
355    fn name(&self) -> &str {
356        "correctness"
357    }
358
359    fn critique(&self, output: &str, _context: &ExecutionContext) -> CritiqueResult {
360        let start = std::time::Instant::now();
361
362        if output.is_empty() {
363            return CritiqueResult::fail(self.name(), 0.0, "Empty output").with_issue(
364                CritiqueIssue::new("No output provided", 1.0, IssueCategory::Missing),
365            );
366        }
367
368        let mut issues = Vec::new();
369        let mut strengths = Vec::new();
370
371        // Check for errors
372        issues.extend(self.check_for_errors(output));
373
374        // Check logic
375        issues.extend(self.check_logic(output));
376
377        // Identify strengths
378        if output.contains("Result<") || output.contains("Option<") {
379            strengths.push("Uses proper error handling types".to_string());
380        }
381        if output.contains("#[test]") {
382            strengths.push("Includes tests".to_string());
383        }
384        if output.contains("///") || output.contains("//!") {
385            strengths.push("Includes documentation".to_string());
386        }
387
388        // Calculate score
389        let issue_penalty: f32 = issues.iter().map(|i| i.severity * 0.15).sum();
390        let score = (1.0 - issue_penalty).clamp(0.0, 1.0);
391        let passed = score >= self.config.pass_threshold;
392
393        let summary = if passed {
394            format!(
395                "Code appears correct with {} minor issue(s)",
396                issues.iter().filter(|i| i.severity < 0.5).count()
397            )
398        } else {
399            format!("Found {} issue(s) affecting correctness", issues.len())
400        };
401
402        let mut result = if passed {
403            CritiqueResult::pass(self.name(), score, summary)
404        } else {
405            CritiqueResult::fail(self.name(), score, summary)
406        };
407
408        result.issues = issues;
409        result.strengths = strengths;
410        result.critique_time_ms = start.elapsed().as_millis() as u64;
411        result
412    }
413
414    fn config(&self) -> &PerspectiveConfig {
415        &self.config
416    }
417}
418
419/// Completeness checker perspective
420///
421/// Checks if all requirements are addressed and the output is complete
422pub struct CompletenessChecker {
423    config: PerspectiveConfig,
424}
425
426impl CompletenessChecker {
427    /// Create a new completeness checker
428    pub fn new() -> Self {
429        Self {
430            config: PerspectiveConfig {
431                weight: 1.0,
432                pass_threshold: 0.6,
433                detailed_feedback: true,
434                custom_checks: Vec::new(),
435            },
436        }
437    }
438
439    /// Create with custom config
440    pub fn with_config(config: PerspectiveConfig) -> Self {
441        Self { config }
442    }
443
444    /// Extract requirements from task
445    fn extract_requirements(&self, task: &str) -> Vec<String> {
446        let mut requirements = Vec::new();
447
448        // Look for action verbs
449        let action_words = [
450            "implement",
451            "create",
452            "add",
453            "build",
454            "write",
455            "define",
456            "include",
457            "support",
458            "handle",
459            "return",
460            "take",
461            "accept",
462        ];
463
464        for word in action_words {
465            if task.to_lowercase().contains(word) {
466                requirements.push(format!("Task mentions '{}' action", word));
467            }
468        }
469
470        // Look for specific features mentioned
471        if task.contains("error handling") || task.contains("handle error") {
472            requirements.push("Error handling".to_string());
473        }
474        if task.contains("test") {
475            requirements.push("Tests".to_string());
476        }
477        if task.contains("document") {
478            requirements.push("Documentation".to_string());
479        }
480        if task.contains("async") {
481            requirements.push("Async support".to_string());
482        }
483
484        requirements
485    }
486
487    /// Check if requirements are met
488    fn check_requirements(&self, output: &str, requirements: &[String]) -> Vec<CritiqueIssue> {
489        let mut issues = Vec::new();
490        let output_lower = output.to_lowercase();
491
492        for req in requirements {
493            let req_lower = req.to_lowercase();
494
495            // Simple keyword matching for requirement fulfillment
496            let is_met = req_lower
497                .split_whitespace()
498                .any(|word| word.len() > 3 && output_lower.contains(word));
499
500            if !is_met {
501                issues.push(
502                    CritiqueIssue::new(
503                        format!("Requirement may not be addressed: {}", req),
504                        0.4,
505                        IssueCategory::Missing,
506                    )
507                    .suggest(format!("Ensure {} is implemented", req)),
508                );
509            }
510        }
511
512        issues
513    }
514
515    /// Check for incomplete markers
516    fn check_incomplete_markers(&self, output: &str) -> Vec<CritiqueIssue> {
517        let mut issues = Vec::new();
518
519        let markers = [
520            ("TODO", "Incomplete TODO item"),
521            ("FIXME", "Incomplete FIXME item"),
522            ("XXX", "XXX marker present"),
523            ("HACK", "Temporary hack present"),
524            ("...", "Ellipsis indicating incomplete"),
525            ("// ...", "Code omitted marker"),
526            ("/* ... */", "Code omitted block"),
527        ];
528
529        for (marker, description) in markers {
530            if output.contains(marker) {
531                let count = output.matches(marker).count();
532                issues.push(
533                    CritiqueIssue::new(
534                        format!("{} ({} occurrence(s))", description, count),
535                        0.5,
536                        IssueCategory::Missing,
537                    )
538                    .suggest(format!("Complete or remove {} markers", marker)),
539                );
540            }
541        }
542
543        issues
544    }
545}
546
547impl Default for CompletenessChecker {
548    fn default() -> Self {
549        Self::new()
550    }
551}
552
553impl Perspective for CompletenessChecker {
554    fn name(&self) -> &str {
555        "completeness"
556    }
557
558    fn critique(&self, output: &str, context: &ExecutionContext) -> CritiqueResult {
559        let start = std::time::Instant::now();
560
561        if output.is_empty() {
562            return CritiqueResult::fail(self.name(), 0.0, "Empty output - nothing completed")
563                .with_issue(CritiqueIssue::new(
564                    "No output provided",
565                    1.0,
566                    IssueCategory::Missing,
567                ));
568        }
569
570        let mut issues = Vec::new();
571        let mut strengths = Vec::new();
572
573        // Extract and check requirements
574        let requirements = self.extract_requirements(&context.task);
575        issues.extend(self.check_requirements(output, &requirements));
576
577        // Check for incomplete markers
578        issues.extend(self.check_incomplete_markers(output));
579
580        // Check output length as proxy for completeness
581        let line_count = output.lines().count();
582        if line_count < 5 && context.task.len() > 50 {
583            issues.push(
584                CritiqueIssue::new(
585                    "Output may be too brief for the task complexity",
586                    0.3,
587                    IssueCategory::Missing,
588                )
589                .suggest("Consider expanding the implementation"),
590            );
591        }
592
593        // Identify completeness strengths
594        if !output.contains("TODO") && !output.contains("FIXME") {
595            strengths.push("No incomplete TODO/FIXME markers".to_string());
596        }
597        if output.lines().count() > 20 {
598            strengths.push("Substantial implementation provided".to_string());
599        }
600
601        // Calculate score
602        let issue_penalty: f32 = issues.iter().map(|i| i.severity * 0.2).sum();
603        let score = (1.0 - issue_penalty).clamp(0.0, 1.0);
604        let passed = score >= self.config.pass_threshold;
605
606        let summary = if passed {
607            "Output appears complete with all major requirements addressed"
608        } else {
609            "Output may be incomplete - some requirements not clearly addressed"
610        };
611
612        let mut result = if passed {
613            CritiqueResult::pass(self.name(), score, summary)
614        } else {
615            CritiqueResult::fail(self.name(), score, summary)
616        };
617
618        result.issues = issues;
619        result.strengths = strengths;
620        result.critique_time_ms = start.elapsed().as_millis() as u64;
621        result
622    }
623
624    fn config(&self) -> &PerspectiveConfig {
625        &self.config
626    }
627}
628
629/// Consistency checker perspective
630///
631/// Ensures internal consistency and adherence to conventions
632pub struct ConsistencyChecker {
633    config: PerspectiveConfig,
634}
635
636impl ConsistencyChecker {
637    /// Create a new consistency checker
638    pub fn new() -> Self {
639        Self {
640            config: PerspectiveConfig {
641                weight: 0.8, // Slightly lower weight
642                pass_threshold: 0.5,
643                detailed_feedback: true,
644                custom_checks: Vec::new(),
645            },
646        }
647    }
648
649    /// Create with custom config
650    pub fn with_config(config: PerspectiveConfig) -> Self {
651        Self { config }
652    }
653
654    /// Check naming conventions
655    fn check_naming(&self, output: &str) -> Vec<CritiqueIssue> {
656        let mut issues = Vec::new();
657
658        // Check for mixed naming conventions (simple heuristic)
659        let _has_snake_case = output.contains("_") && output.contains("fn ");
660        let has_camel_case = output
661            .chars()
662            .zip(output.chars().skip(1))
663            .any(|(a, b)| a.is_lowercase() && b.is_uppercase());
664
665        // In Rust, we expect snake_case for functions/variables
666        if has_camel_case && output.contains("fn ") && !output.contains("trait ") {
667            issues.push(
668                CritiqueIssue::new(
669                    "Possible camelCase usage in Rust code (should use snake_case)",
670                    0.3,
671                    IssueCategory::Style,
672                )
673                .suggest("Use snake_case for function and variable names"),
674            );
675        }
676
677        issues
678    }
679
680    /// Check for consistent formatting
681    fn check_formatting(&self, output: &str) -> Vec<CritiqueIssue> {
682        let mut issues = Vec::new();
683
684        // Check for inconsistent indentation
685        let lines: Vec<&str> = output.lines().collect();
686        let mut indent_styles = HashMap::new();
687
688        for line in &lines {
689            if line.starts_with("    ") {
690                *indent_styles.entry("4spaces").or_insert(0) += 1;
691            } else if line.starts_with("  ") && !line.starts_with("    ") {
692                *indent_styles.entry("2spaces").or_insert(0) += 1;
693            } else if line.starts_with('\t') {
694                *indent_styles.entry("tabs").or_insert(0) += 1;
695            }
696        }
697
698        if indent_styles.len() > 1 {
699            issues.push(
700                CritiqueIssue::new(
701                    "Inconsistent indentation style detected",
702                    0.4,
703                    IssueCategory::Style,
704                )
705                .suggest("Use consistent indentation (4 spaces recommended for Rust)"),
706            );
707        }
708
709        // Check for trailing whitespace
710        let trailing_ws_count = lines.iter().filter(|l| l.ends_with(' ')).count();
711        if trailing_ws_count > 0 {
712            issues.push(
713                CritiqueIssue::new(
714                    format!("Trailing whitespace on {} line(s)", trailing_ws_count),
715                    0.2,
716                    IssueCategory::Style,
717                )
718                .suggest("Remove trailing whitespace"),
719            );
720        }
721
722        issues
723    }
724
725    /// Check for internal consistency
726    fn check_internal_consistency(&self, output: &str) -> Vec<CritiqueIssue> {
727        let mut issues = Vec::new();
728
729        // Check for mix of error handling styles
730        let uses_result = output.contains("Result<");
731        let uses_option = output.contains("Option<");
732        let uses_unwrap = output.contains(".unwrap()");
733        let uses_question = output.contains("?;") || output.contains("?)");
734
735        if (uses_result || uses_option) && uses_unwrap && uses_question {
736            issues.push(
737                CritiqueIssue::new(
738                    "Inconsistent error handling: mixing ? operator and unwrap()",
739                    0.4,
740                    IssueCategory::Inconsistent,
741                )
742                .suggest("Prefer using ? operator consistently for error propagation"),
743            );
744        }
745
746        // Check for consistent visibility modifiers
747        let pub_count = output.matches("pub fn").count();
748        let priv_count = output.matches("fn ").count() - pub_count;
749
750        if pub_count > 0
751            && priv_count > 0
752            && (pub_count as f32 / (pub_count + priv_count) as f32) < 0.3
753        {
754            // This is actually fine, just noting it
755        }
756
757        issues
758    }
759}
760
761impl Default for ConsistencyChecker {
762    fn default() -> Self {
763        Self::new()
764    }
765}
766
767impl Perspective for ConsistencyChecker {
768    fn name(&self) -> &str {
769        "consistency"
770    }
771
772    fn critique(&self, output: &str, _context: &ExecutionContext) -> CritiqueResult {
773        let start = std::time::Instant::now();
774
775        if output.is_empty() {
776            return CritiqueResult::fail(self.name(), 0.0, "Empty output").with_issue(
777                CritiqueIssue::new(
778                    "No output to check consistency",
779                    1.0,
780                    IssueCategory::Missing,
781                ),
782            );
783        }
784
785        let mut issues = Vec::new();
786        let mut strengths = Vec::new();
787
788        // Check naming conventions
789        issues.extend(self.check_naming(output));
790
791        // Check formatting
792        issues.extend(self.check_formatting(output));
793
794        // Check internal consistency
795        issues.extend(self.check_internal_consistency(output));
796
797        // Identify strengths
798        if !issues
799            .iter()
800            .any(|i| i.category == IssueCategory::Inconsistent)
801        {
802            strengths.push("Consistent coding style".to_string());
803        }
804        if output.contains("use std::") || output.contains("use crate::") {
805            strengths.push("Proper import organization".to_string());
806        }
807
808        // Calculate score
809        let issue_penalty: f32 = issues.iter().map(|i| i.severity * 0.15).sum();
810        let score = (1.0 - issue_penalty).clamp(0.0, 1.0);
811        let passed = score >= self.config.pass_threshold;
812
813        let summary = if passed {
814            "Code follows consistent conventions and style"
815        } else {
816            "Inconsistencies detected in style or conventions"
817        };
818
819        let mut result = if passed {
820            CritiqueResult::pass(self.name(), score, summary)
821        } else {
822            CritiqueResult::fail(self.name(), score, summary)
823        };
824
825        result.issues = issues;
826        result.strengths = strengths;
827        result.critique_time_ms = start.elapsed().as_millis() as u64;
828        result
829    }
830
831    fn config(&self) -> &PerspectiveConfig {
832        &self.config
833    }
834}
835
836/// Unified critique combining multiple perspectives
837#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct UnifiedCritique {
839    /// Individual critique results
840    pub critiques: Vec<CritiqueResult>,
841    /// Overall pass/fail
842    pub passed: bool,
843    /// Combined score (weighted average)
844    pub combined_score: f32,
845    /// Overall summary
846    pub summary: String,
847    /// Prioritized issues (sorted by severity)
848    pub prioritized_issues: Vec<CritiqueIssue>,
849    /// All identified strengths
850    pub strengths: Vec<String>,
851    /// Total critique time
852    pub total_time_ms: u64,
853}
854
855impl UnifiedCritique {
856    /// Create a unified critique from multiple perspective results
857    pub fn combine(critiques: Vec<CritiqueResult>, weights: &[f32]) -> Self {
858        let mut total_weight = 0.0f32;
859        let mut weighted_sum = 0.0f32;
860        let mut all_issues = Vec::new();
861        let mut all_strengths = Vec::new();
862        let mut total_time = 0u64;
863
864        for (i, critique) in critiques.iter().enumerate() {
865            let weight = weights.get(i).copied().unwrap_or(1.0);
866            total_weight += weight;
867            weighted_sum += critique.score * weight;
868
869            all_issues.extend(critique.issues.clone());
870            all_strengths.extend(critique.strengths.clone());
871            total_time += critique.critique_time_ms;
872        }
873
874        let combined_score = if total_weight > 0.0 {
875            weighted_sum / total_weight
876        } else {
877            0.0
878        };
879
880        // Sort issues by severity
881        all_issues.sort_by(|a, b| {
882            b.severity
883                .partial_cmp(&a.severity)
884                .unwrap_or(std::cmp::Ordering::Equal)
885        });
886
887        // Deduplicate strengths
888        all_strengths.sort();
889        all_strengths.dedup();
890
891        let pass_count = critiques.iter().filter(|c| c.passed).count();
892        let passed = pass_count > critiques.len() / 2 && combined_score >= 0.6;
893
894        let summary = if passed {
895            format!(
896                "Passed {}/{} perspectives with combined score {:.2}",
897                pass_count,
898                critiques.len(),
899                combined_score
900            )
901        } else {
902            format!(
903                "Failed: only {}/{} perspectives passed, combined score {:.2}",
904                pass_count,
905                critiques.len(),
906                combined_score
907            )
908        };
909
910        Self {
911            critiques,
912            passed,
913            combined_score,
914            summary,
915            prioritized_issues: all_issues,
916            strengths: all_strengths,
917            total_time_ms: total_time,
918        }
919    }
920
921    /// Get the top N issues
922    pub fn top_issues(&self, n: usize) -> Vec<&CritiqueIssue> {
923        self.prioritized_issues.iter().take(n).collect()
924    }
925
926    /// Get issues by category
927    pub fn issues_by_category(&self, category: IssueCategory) -> Vec<&CritiqueIssue> {
928        self.prioritized_issues
929            .iter()
930            .filter(|i| i.category == category)
931            .collect()
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use crate::claude_flow::AgentType;
939
940    fn test_context() -> ExecutionContext {
941        ExecutionContext::new("implement a function", AgentType::Coder, "test input")
942    }
943
944    #[test]
945    fn test_critique_result_builders() {
946        let pass = CritiqueResult::pass("test", 0.8, "Good job").with_strength("Clean code");
947        assert!(pass.passed);
948        assert!(!pass.strengths.is_empty());
949
950        let fail = CritiqueResult::fail("test", 0.3, "Issues found")
951            .with_issue(CritiqueIssue::new("Problem", 0.7, IssueCategory::Logic));
952        assert!(!fail.passed);
953        assert!(!fail.issues.is_empty());
954    }
955
956    #[test]
957    fn test_critique_issue_builder() {
958        let issue = CritiqueIssue::new("Test issue", 0.5, IssueCategory::Logic)
959            .at("line 5")
960            .suggest("Fix it");
961
962        assert_eq!(issue.location, Some("line 5".to_string()));
963        assert!(!issue.suggestion.is_empty());
964    }
965
966    #[test]
967    fn test_correctness_checker_empty() {
968        let checker = CorrectnessChecker::new();
969        let context = test_context();
970        let result = checker.critique("", &context);
971
972        assert!(!result.passed);
973        assert!(result.score < 0.5);
974    }
975
976    #[test]
977    fn test_correctness_checker_with_errors() {
978        let checker = CorrectnessChecker::new();
979        let context = test_context();
980        let output = r#"
981            fn test() {
982                panic!("error");
983                todo!();
984            }
985        "#;
986
987        let result = checker.critique(output, &context);
988        assert!(!result.issues.is_empty());
989    }
990
991    #[test]
992    fn test_correctness_checker_clean_code() {
993        let checker = CorrectnessChecker::new();
994        let context = test_context();
995        let output = r#"
996            /// Documentation
997            pub fn example() -> Result<(), Error> {
998                Ok(())
999            }
1000
1001            #[test]
1002            fn test_example() {
1003                assert!(example().is_ok());
1004            }
1005        "#;
1006
1007        let result = checker.critique(output, &context);
1008        assert!(!result.strengths.is_empty());
1009    }
1010
1011    #[test]
1012    fn test_completeness_checker_todo() {
1013        let checker = CompletenessChecker::new();
1014        let context = test_context();
1015        let output = "fn example() { // TODO: implement }";
1016
1017        let result = checker.critique(output, &context);
1018        assert!(result
1019            .issues
1020            .iter()
1021            .any(|i| i.category == IssueCategory::Missing));
1022    }
1023
1024    #[test]
1025    fn test_completeness_checker_complete() {
1026        let checker = CompletenessChecker::new();
1027        let context = ExecutionContext::new("implement function", AgentType::Coder, "input");
1028        let output = r#"
1029            pub fn implement_function() -> i32 {
1030                let value = 42;
1031                // Full implementation here
1032                value * 2
1033            }
1034        "#;
1035
1036        let result = checker.critique(output, &context);
1037        assert!(result.passed || result.score > 0.5);
1038    }
1039
1040    #[test]
1041    fn test_consistency_checker_mixed_indent() {
1042        let checker = ConsistencyChecker::new();
1043        let context = test_context();
1044        let output = "fn test() {\n    line1\n  line2\n\tline3\n}";
1045
1046        let result = checker.critique(output, &context);
1047        assert!(result
1048            .issues
1049            .iter()
1050            .any(|i| i.category == IssueCategory::Style));
1051    }
1052
1053    #[test]
1054    fn test_consistency_checker_clean() {
1055        let checker = ConsistencyChecker::new();
1056        let context = test_context();
1057        let output = r#"
1058use std::io;
1059
1060fn clean_function() -> io::Result<()> {
1061    let value = 42;
1062    Ok(())
1063}
1064        "#;
1065
1066        let result = checker.critique(output, &context);
1067        // Should pass or have high score
1068        assert!(result.score > 0.5);
1069    }
1070
1071    #[test]
1072    fn test_unified_critique() {
1073        let correctness = CritiqueResult::pass("correctness", 0.8, "Good");
1074        let completeness = CritiqueResult::pass("completeness", 0.7, "Complete");
1075        let consistency = CritiqueResult::fail("consistency", 0.4, "Issues");
1076
1077        let unified = UnifiedCritique::combine(
1078            vec![correctness, completeness, consistency],
1079            &[1.2, 1.0, 0.8],
1080        );
1081
1082        assert!(unified.combined_score > 0.5);
1083        assert!(!unified.summary.is_empty());
1084    }
1085
1086    #[test]
1087    fn test_unified_critique_issues_by_category() {
1088        let mut result = CritiqueResult::fail("test", 0.5, "Issues")
1089            .with_issue(CritiqueIssue::new("Logic issue", 0.7, IssueCategory::Logic))
1090            .with_issue(CritiqueIssue::new("Style issue", 0.3, IssueCategory::Style));
1091
1092        let unified = UnifiedCritique::combine(vec![result], &[1.0]);
1093
1094        let logic_issues = unified.issues_by_category(IssueCategory::Logic);
1095        assert_eq!(logic_issues.len(), 1);
1096    }
1097
1098    #[test]
1099    fn test_perspective_trait_implementation() {
1100        let checker: Box<dyn Perspective> = Box::new(CorrectnessChecker::new());
1101        assert_eq!(checker.name(), "correctness");
1102
1103        let context = test_context();
1104        let result = checker.critique("fn test() {}", &context);
1105        assert!(!result.perspective_name.is_empty());
1106    }
1107}