1use super::reflective_agent::ExecutionContext;
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PerspectiveConfig {
40 pub weight: f32,
42 pub pass_threshold: f32,
44 pub detailed_feedback: bool,
46 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#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct CritiqueResult {
64 pub perspective_name: String,
66 pub passed: bool,
68 pub score: f32,
70 pub summary: String,
72 pub issues: Vec<CritiqueIssue>,
74 pub strengths: Vec<String>,
76 pub critique_time_ms: u64,
78}
79
80impl CritiqueResult {
81 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 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 pub fn with_issue(mut self, issue: CritiqueIssue) -> Self {
109 self.issues.push(issue);
110 self
111 }
112
113 pub fn with_strength(mut self, strength: impl Into<String>) -> Self {
115 self.strengths.push(strength.into());
116 self
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CritiqueIssue {
123 pub severity: f32,
125 pub description: String,
127 pub location: Option<String>,
129 pub suggestion: String,
131 pub category: IssueCategory,
133}
134
135impl CritiqueIssue {
136 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 pub fn at(mut self, location: impl Into<String>) -> Self {
149 self.location = Some(location.into());
150 self
151 }
152
153 pub fn suggest(mut self, suggestion: impl Into<String>) -> Self {
155 self.suggestion = suggestion.into();
156 self
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
162pub enum IssueCategory {
163 Logic,
165 Syntax,
167 Missing,
169 Redundant,
171 Inconsistent,
173 Style,
175 Security,
177 Performance,
179 Documentation,
181 Other,
183}
184
185pub trait Perspective: Send + Sync {
187 fn name(&self) -> &str;
189
190 fn critique(&self, output: &str, context: &ExecutionContext) -> CritiqueResult;
192
193 fn config(&self) -> &PerspectiveConfig;
195}
196
197pub struct CorrectnessChecker {
201 config: PerspectiveConfig,
202}
203
204impl CorrectnessChecker {
205 pub fn new() -> Self {
207 Self {
208 config: PerspectiveConfig {
209 weight: 1.2, pass_threshold: 0.7,
211 detailed_feedback: true,
212 custom_checks: Vec::new(),
213 },
214 }
215 }
216
217 pub fn with_config(config: PerspectiveConfig) -> Self {
219 Self { config }
220 }
221
222 fn check_for_errors(&self, output: &str) -> Vec<CritiqueIssue> {
224 let mut issues = Vec::new();
225
226 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 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 fn check_logic(&self, output: &str) -> Vec<CritiqueIssue> {
310 let mut issues = Vec::new();
311
312 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 let empty_fn_pattern = "fn ";
326 if output.contains(empty_fn_pattern) {
327 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 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 issues.extend(self.check_for_errors(output));
373
374 issues.extend(self.check_logic(output));
376
377 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 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
419pub struct CompletenessChecker {
423 config: PerspectiveConfig,
424}
425
426impl CompletenessChecker {
427 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 pub fn with_config(config: PerspectiveConfig) -> Self {
441 Self { config }
442 }
443
444 fn extract_requirements(&self, task: &str) -> Vec<String> {
446 let mut requirements = Vec::new();
447
448 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 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 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 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 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 let requirements = self.extract_requirements(&context.task);
575 issues.extend(self.check_requirements(output, &requirements));
576
577 issues.extend(self.check_incomplete_markers(output));
579
580 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 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 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
629pub struct ConsistencyChecker {
633 config: PerspectiveConfig,
634}
635
636impl ConsistencyChecker {
637 pub fn new() -> Self {
639 Self {
640 config: PerspectiveConfig {
641 weight: 0.8, pass_threshold: 0.5,
643 detailed_feedback: true,
644 custom_checks: Vec::new(),
645 },
646 }
647 }
648
649 pub fn with_config(config: PerspectiveConfig) -> Self {
651 Self { config }
652 }
653
654 fn check_naming(&self, output: &str) -> Vec<CritiqueIssue> {
656 let mut issues = Vec::new();
657
658 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 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 fn check_formatting(&self, output: &str) -> Vec<CritiqueIssue> {
682 let mut issues = Vec::new();
683
684 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 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 fn check_internal_consistency(&self, output: &str) -> Vec<CritiqueIssue> {
727 let mut issues = Vec::new();
728
729 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 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 }
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 issues.extend(self.check_naming(output));
790
791 issues.extend(self.check_formatting(output));
793
794 issues.extend(self.check_internal_consistency(output));
796
797 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct UnifiedCritique {
839 pub critiques: Vec<CritiqueResult>,
841 pub passed: bool,
843 pub combined_score: f32,
845 pub summary: String,
847 pub prioritized_issues: Vec<CritiqueIssue>,
849 pub strengths: Vec<String>,
851 pub total_time_ms: u64,
853}
854
855impl UnifiedCritique {
856 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 all_issues.sort_by(|a, b| {
882 b.severity
883 .partial_cmp(&a.severity)
884 .unwrap_or(std::cmp::Ordering::Equal)
885 });
886
887 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 pub fn top_issues(&self, n: usize) -> Vec<&CritiqueIssue> {
923 self.prioritized_issues.iter().take(n).collect()
924 }
925
926 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 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}