1use super::learning::LearningCoordinator;
55use super::query_core::{QueryCore, QueryExpr, QueryOp, QueryResult, RelationType};
56use brainwires_core::graph::RelationshipGraphT;
57use std::collections::HashMap;
58
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub enum ErrorType {
62 EmptyResult,
64 ResultOverflow,
66 EntityNotFound(String),
68 RelationMismatch(String),
70 CoreferenceFailure(String),
72 SchemaAlignment(String),
74 Timeout,
76 Unknown(String),
78}
79
80impl ErrorType {
81 pub fn description(&self) -> String {
83 match self {
84 ErrorType::EmptyResult => "Query returned no results".to_string(),
85 ErrorType::ResultOverflow => "Query returned too many results".to_string(),
86 ErrorType::EntityNotFound(name) => format!("Entity '{}' not found", name),
87 ErrorType::RelationMismatch(rel) => format!("Relationship '{}' does not apply", rel),
88 ErrorType::CoreferenceFailure(ref_text) => {
89 format!("Could not resolve reference '{}'", ref_text)
90 }
91 ErrorType::SchemaAlignment(msg) => format!("Schema alignment issue: {}", msg),
92 ErrorType::Timeout => "Query execution timed out".to_string(),
93 ErrorType::Unknown(msg) => format!("Unknown error: {}", msg),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
100pub enum Severity {
101 Info,
103 Warning,
105 Error,
107 Critical,
109}
110
111impl Severity {
112 pub fn as_str(&self) -> &'static str {
114 match self {
115 Severity::Info => "info",
116 Severity::Warning => "warning",
117 Severity::Error => "error",
118 Severity::Critical => "critical",
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct Issue {
126 pub error_type: ErrorType,
128 pub severity: Severity,
130 pub message: String,
132 pub suggested_fixes: Vec<SuggestedFix>,
134 pub source: Option<String>,
136}
137
138impl Issue {
139 pub fn new(error_type: ErrorType, severity: Severity, message: &str) -> Self {
141 Self {
142 error_type,
143 severity,
144 message: message.to_string(),
145 suggested_fixes: Vec::new(),
146 source: None,
147 }
148 }
149
150 pub fn with_fix(mut self, fix: SuggestedFix) -> Self {
152 self.suggested_fixes.push(fix);
153 self
154 }
155
156 pub fn with_source(mut self, source: &str) -> Self {
158 self.source = Some(source.to_string());
159 self
160 }
161}
162
163#[derive(Debug, Clone)]
165pub enum SuggestedFix {
166 RetryWithQuery(QueryCore),
168 ExpandScope {
170 relation: String,
172 },
173 NarrowScope {
175 filter: String,
177 },
178 ResolveEntity {
180 original: String,
182 suggested: String,
184 },
185 AddRelation {
187 from: String,
189 to: String,
191 relation: String,
193 },
194 ManualIntervention(String),
196}
197
198impl SuggestedFix {
199 pub fn description(&self) -> String {
201 match self {
202 SuggestedFix::RetryWithQuery(_) => "Retry with modified query".to_string(),
203 SuggestedFix::ExpandScope { relation } => {
204 format!("Expand scope to include {} relationships", relation)
205 }
206 SuggestedFix::NarrowScope { filter } => {
207 format!("Narrow scope with filter: {}", filter)
208 }
209 SuggestedFix::ResolveEntity {
210 original,
211 suggested,
212 } => {
213 format!("Resolve '{}' as '{}'", original, suggested)
214 }
215 SuggestedFix::AddRelation { from, to, relation } => {
216 format!("Add {} relationship from {} to {}", relation, from, to)
217 }
218 SuggestedFix::ManualIntervention(msg) => format!("Manual intervention: {}", msg),
219 }
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct CorrectionRecord {
226 pub issue: Issue,
228 pub fix_applied: SuggestedFix,
230 pub success: bool,
232 pub timestamp: i64,
234}
235
236#[derive(Debug, Clone)]
238pub struct ReflectionReport {
239 pub query: QueryCore,
241 pub result: QueryResult,
243 pub issues: Vec<Issue>,
245 pub quality_score: f32,
247 pub correction_attempted: bool,
249 pub corrected_query: Option<QueryCore>,
251 pub corrected_result: Option<QueryResult>,
253}
254
255impl ReflectionReport {
256 pub fn new(query: QueryCore, result: QueryResult) -> Self {
258 Self {
259 query,
260 result,
261 issues: Vec::new(),
262 quality_score: 1.0,
263 correction_attempted: false,
264 corrected_query: None,
265 corrected_result: None,
266 }
267 }
268
269 pub fn is_acceptable(&self) -> bool {
271 self.quality_score >= 0.5 && !self.issues.iter().any(|i| i.severity >= Severity::Error)
272 }
273
274 pub fn max_severity(&self) -> Option<Severity> {
276 self.issues.iter().map(|i| i.severity).max()
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct ReflectionConfig {
283 pub max_results: usize,
285 pub min_results: usize,
287 pub max_retries: u32,
289 pub auto_correct: bool,
291}
292
293impl Default for ReflectionConfig {
294 fn default() -> Self {
295 Self {
296 max_results: 100,
297 min_results: 1,
298 max_retries: 2,
299 auto_correct: true,
300 }
301 }
302}
303
304pub struct ReflectionModule {
306 config: ReflectionConfig,
308 error_patterns: HashMap<ErrorType, u32>,
310 correction_history: Vec<CorrectionRecord>,
312}
313
314impl ReflectionModule {
315 pub fn new(config: ReflectionConfig) -> Self {
317 Self {
318 config,
319 error_patterns: HashMap::new(),
320 correction_history: Vec::new(),
321 }
322 }
323
324 pub fn analyze(
326 &mut self,
327 query: &QueryCore,
328 result: &QueryResult,
329 graph: &dyn RelationshipGraphT,
330 ) -> ReflectionReport {
331 let mut report = ReflectionReport::new(query.clone(), result.clone());
332
333 if let Some(ref error) = result.error {
335 report.issues.push(Issue::new(
336 ErrorType::Unknown(error.clone()),
337 Severity::Error,
338 error,
339 ));
340 report.quality_score = 0.0;
341 return report;
342 }
343
344 if result.values.is_empty() && result.count != Some(0) {
346 let issue = self.analyze_empty_result(query, graph);
347 report.issues.push(issue);
348 report.quality_score = 0.3;
349 }
350
351 if result.values.len() > self.config.max_results {
353 let issue = Issue::new(
354 ErrorType::ResultOverflow,
355 Severity::Warning,
356 &format!(
357 "Query returned {} results (max: {})",
358 result.values.len(),
359 self.config.max_results
360 ),
361 )
362 .with_fix(SuggestedFix::NarrowScope {
363 filter: "Add type or name filter".to_string(),
364 });
365 report.issues.push(issue);
366 report.quality_score = 0.6;
367 }
368
369 for (entity_name, _entity_type) in &query.entities {
371 if graph.get_node(entity_name).is_none() {
372 let similar = self.find_similar_entities(entity_name, graph);
373 let mut issue = Issue::new(
374 ErrorType::EntityNotFound(entity_name.clone()),
375 Severity::Warning,
376 &format!("Entity '{}' not found in graph", entity_name),
377 );
378
379 if let Some(suggestion) = similar.first() {
380 issue = issue.with_fix(SuggestedFix::ResolveEntity {
381 original: entity_name.clone(),
382 suggested: suggestion.clone(),
383 });
384 }
385
386 report.issues.push(issue);
387 report.quality_score = (report.quality_score - 0.2).max(0.0);
388 }
389 }
390
391 self.validate_relationships(query, graph, &mut report);
393
394 for issue in &report.issues {
396 *self
397 .error_patterns
398 .entry(issue.error_type.clone())
399 .or_insert(0) += 1;
400 }
401
402 report
403 }
404
405 fn analyze_empty_result(&self, query: &QueryCore, graph: &dyn RelationshipGraphT) -> Issue {
407 for (entity_name, _) in &query.entities {
409 if graph.get_node(entity_name).is_none() {
410 return Issue::new(
411 ErrorType::EntityNotFound(entity_name.clone()),
412 Severity::Error,
413 &format!(
414 "Entity '{}' not found - query cannot return results",
415 entity_name
416 ),
417 );
418 }
419 }
420
421 if let Some(relation_msg) = self.check_relationship_applicability(&query.root, graph) {
423 return Issue::new(
424 ErrorType::RelationMismatch(relation_msg.clone()),
425 Severity::Error,
426 &relation_msg,
427 )
428 .with_fix(SuggestedFix::ExpandScope {
429 relation: "CoOccurs".to_string(),
430 });
431 }
432
433 Issue::new(
435 ErrorType::EmptyResult,
436 Severity::Warning,
437 "Query returned no results",
438 )
439 .with_fix(SuggestedFix::ExpandScope {
440 relation: "All".to_string(),
441 })
442 }
443
444 fn check_relationship_applicability(
446 &self,
447 expr: &QueryExpr,
448 graph: &dyn RelationshipGraphT,
449 ) -> Option<String> {
450 match expr {
451 QueryExpr::Op(QueryOp::Join {
452 relation,
453 subject,
454 object,
455 }) => {
456 let subject_name = match subject.as_ref() {
458 QueryExpr::Constant(name, _) => Some(name.as_str()),
459 _ => None,
460 };
461 let object_name = match object.as_ref() {
462 QueryExpr::Constant(name, _) => Some(name.as_str()),
463 _ => None,
464 };
465
466 if let Some(name) = subject_name.or(object_name) {
468 let edges = graph.get_edges(name);
469 if let Some(edge_type) = relation.to_edge_type()
470 && !edges.iter().any(|e| e.edge_type == edge_type)
471 {
472 return Some(format!(
473 "No {:?} relationships found for '{}'",
474 relation, name
475 ));
476 }
477 }
478
479 None
480 }
481 _ => None,
482 }
483 }
484
485 fn find_similar_entities(&self, name: &str, graph: &dyn RelationshipGraphT) -> Vec<String> {
487 let candidates = graph.search(name, 5);
488 candidates
489 .iter()
490 .map(|node| node.entity_name.clone())
491 .collect()
492 }
493
494 fn validate_relationships(
496 &self,
497 query: &QueryCore,
498 graph: &dyn RelationshipGraphT,
499 report: &mut ReflectionReport,
500 ) {
501 self.validate_expr(&query.root, graph, report);
502 }
503
504 #[allow(clippy::only_used_in_recursion)]
505 fn validate_expr(
506 &self,
507 expr: &QueryExpr,
508 graph: &dyn RelationshipGraphT,
509 report: &mut ReflectionReport,
510 ) {
511 match expr {
512 QueryExpr::Op(QueryOp::Join {
513 relation,
514 subject,
515 object,
516 }) => {
517 if relation.to_edge_type().is_none()
519 && !matches!(
520 relation,
521 RelationType::HasType
522 | RelationType::HasError
523 | RelationType::CreatedAt
524 | RelationType::ModifiedAt
525 )
526 && let RelationType::Custom(name) = relation
527 {
528 report.issues.push(
529 Issue::new(
530 ErrorType::RelationMismatch(name.clone()),
531 Severity::Warning,
532 &format!("Custom relationship '{}' may not exist", name),
533 )
534 .with_source(&format!("{:?}", relation)),
535 );
536 }
537
538 self.validate_expr(subject, graph, report);
540 self.validate_expr(object, graph, report);
541 }
542 QueryExpr::Op(QueryOp::And(exprs)) | QueryExpr::Op(QueryOp::Or(exprs)) => {
543 for e in exprs {
544 self.validate_expr(e, graph, report);
545 }
546 }
547 QueryExpr::Op(QueryOp::Filter { source, .. }) => {
548 self.validate_expr(source, graph, report);
549 }
550 QueryExpr::Op(QueryOp::Count(inner)) => {
551 self.validate_expr(inner, graph, report);
552 }
553 QueryExpr::Op(QueryOp::Superlative { source, .. }) => {
554 self.validate_expr(source, graph, report);
555 }
556 _ => {}
557 }
558 }
559
560 pub fn validate_query_core(&self, query: &QueryCore) -> Vec<Issue> {
562 let mut issues = Vec::new();
563
564 if query.entities.is_empty() {
566 issues.push(Issue::new(
567 ErrorType::SchemaAlignment("No entities in query".to_string()),
568 Severity::Warning,
569 "Query does not reference any entities",
570 ));
571 }
572
573 if matches!(
575 query.question_type,
576 super::query_core::QuestionType::Unknown
577 ) {
578 issues.push(Issue::new(
579 ErrorType::SchemaAlignment("Unknown question type".to_string()),
580 Severity::Info,
581 "Could not determine question type",
582 ));
583 }
584
585 issues
586 }
587
588 pub fn attempt_correction(
590 &mut self,
591 report: &mut ReflectionReport,
592 graph: &dyn RelationshipGraphT,
593 _executor: &super::query_core::QueryExecutor,
594 ) -> bool {
595 if !self.config.auto_correct {
596 return false;
597 }
598
599 if report.issues.is_empty() {
600 return true; }
602
603 report.correction_attempted = true;
604
605 for issue in &report.issues {
607 if issue.severity < Severity::Warning {
608 continue;
609 }
610
611 for fix in &issue.suggested_fixes {
612 match fix {
613 SuggestedFix::ResolveEntity {
614 original,
615 suggested,
616 } => {
617 if graph.get_node(suggested).is_some() {
619 let corrected =
621 self.substitute_entity(&report.query, original, suggested);
622 if let Some(corrected) = corrected {
623 report.corrected_query = Some(corrected);
624 self.record_correction(issue.clone(), fix.clone(), true);
627 return true;
628 }
629 }
630 }
631 SuggestedFix::ExpandScope { .. } => {
632 }
635 _ => {}
636 }
637 }
638 }
639
640 false
641 }
642
643 fn substitute_entity(
645 &self,
646 query: &QueryCore,
647 original: &str,
648 replacement: &str,
649 ) -> Option<QueryCore> {
650 let mut corrected = query.clone();
651
652 for (name, _) in &mut corrected.entities {
654 if name == original {
655 *name = replacement.to_string();
656 }
657 }
658
659 corrected.root = Self::substitute_in_expr(&query.root, original, replacement);
661
662 Some(corrected)
663 }
664
665 fn substitute_in_expr(expr: &QueryExpr, original: &str, replacement: &str) -> QueryExpr {
666 match expr {
667 QueryExpr::Constant(name, entity_type) => {
668 if name == original {
669 QueryExpr::Constant(replacement.to_string(), entity_type.clone())
670 } else {
671 expr.clone()
672 }
673 }
674 QueryExpr::Op(op) => QueryExpr::Op(match op {
675 QueryOp::Join {
676 relation,
677 subject,
678 object,
679 } => QueryOp::Join {
680 relation: relation.clone(),
681 subject: Box::new(Self::substitute_in_expr(subject, original, replacement)),
682 object: Box::new(Self::substitute_in_expr(object, original, replacement)),
683 },
684 QueryOp::And(exprs) => QueryOp::And(
685 exprs
686 .iter()
687 .map(|e| Self::substitute_in_expr(e, original, replacement))
688 .collect(),
689 ),
690 QueryOp::Or(exprs) => QueryOp::Or(
691 exprs
692 .iter()
693 .map(|e| Self::substitute_in_expr(e, original, replacement))
694 .collect(),
695 ),
696 QueryOp::Filter { source, predicate } => QueryOp::Filter {
697 source: Box::new(Self::substitute_in_expr(source, original, replacement)),
698 predicate: predicate.clone(),
699 },
700 QueryOp::Count(inner) => QueryOp::Count(Box::new(Self::substitute_in_expr(
701 inner,
702 original,
703 replacement,
704 ))),
705 QueryOp::Superlative {
706 source,
707 property,
708 direction,
709 } => QueryOp::Superlative {
710 source: Box::new(Self::substitute_in_expr(source, original, replacement)),
711 property: property.clone(),
712 direction: direction.clone(),
713 },
714 _ => op.clone(),
715 }),
716 _ => expr.clone(),
717 }
718 }
719
720 fn record_correction(&mut self, issue: Issue, fix: SuggestedFix, success: bool) {
722 self.correction_history.push(CorrectionRecord {
723 issue,
724 fix_applied: fix,
725 success,
726 timestamp: chrono::Utc::now().timestamp(),
727 });
728 }
729
730 pub fn provide_feedback(
732 &self,
733 report: &ReflectionReport,
734 coordinator: &mut LearningCoordinator,
735 ) {
736 let success = report.is_acceptable();
738 let result_count = report.result.values.len();
739
740 coordinator.record_outcome(
741 None, success,
743 result_count,
744 Some(&report.query),
745 0, );
747
748 for issue in &report.issues {
750 if issue.severity >= Severity::Error {
751 }
754 }
755 }
756
757 pub fn get_error_stats(&self) -> HashMap<ErrorType, u32> {
759 self.error_patterns.clone()
760 }
761
762 pub fn correction_success_rate(&self) -> f32 {
764 if self.correction_history.is_empty() {
765 return 0.0;
766 }
767
768 let successes = self.correction_history.iter().filter(|r| r.success).count();
769 successes as f32 / self.correction_history.len() as f32
770 }
771}
772
773impl Default for ReflectionModule {
774 fn default() -> Self {
775 Self::new(ReflectionConfig::default())
776 }
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use crate::query_core::{QueryExpr, QueryResultValue, QuestionType};
783 use brainwires_core::graph::EntityType;
784 use brainwires_knowledge::RelationshipGraph;
785
786 fn create_test_query() -> QueryCore {
787 QueryCore::new(
788 QuestionType::Definition,
789 QueryExpr::var("x"),
790 vec![("main.rs".to_string(), EntityType::File)],
791 "What is main.rs?".to_string(),
792 )
793 }
794
795 #[test]
796 fn test_analyze_empty_result() {
797 let mut reflection = ReflectionModule::new(ReflectionConfig::default());
798 let query = create_test_query();
799 let result = QueryResult::empty();
800 let graph = RelationshipGraph::new();
801
802 let report = reflection.analyze(&query, &result, &graph);
803
804 assert!(!report.issues.is_empty());
805 assert!(report.issues.iter().any(|i| matches!(
806 i.error_type,
807 ErrorType::EmptyResult | ErrorType::EntityNotFound(_)
808 )));
809 }
810
811 #[test]
812 fn test_analyze_overflow_result() {
813 let mut reflection = ReflectionModule::new(ReflectionConfig {
814 max_results: 10,
815 ..Default::default()
816 });
817 let query = create_test_query();
818
819 let mut values = Vec::new();
821 for i in 0..20 {
822 values.push(QueryResultValue {
823 value: format!("entity_{}", i),
824 entity_type: Some(EntityType::File),
825 score: 0.8,
826 metadata: std::collections::HashMap::new(),
827 });
828 }
829 let result = crate::query_core::QueryResult::with_values(values);
830 let graph = RelationshipGraph::new();
831
832 let report = reflection.analyze(&query, &result, &graph);
833
834 assert!(
835 report
836 .issues
837 .iter()
838 .any(|i| i.error_type == ErrorType::ResultOverflow)
839 );
840 }
841
842 #[test]
843 fn test_validate_query_core() {
844 let reflection = ReflectionModule::new(ReflectionConfig::default());
845
846 let query = QueryCore::new(
848 QuestionType::Unknown,
849 QueryExpr::var("x"),
850 vec![],
851 "Test".to_string(),
852 );
853
854 let issues = reflection.validate_query_core(&query);
855 assert!(!issues.is_empty());
856 }
857
858 #[test]
859 fn test_issue_creation() {
860 let issue = Issue::new(ErrorType::EmptyResult, Severity::Warning, "No results")
861 .with_fix(SuggestedFix::ExpandScope {
862 relation: "All".to_string(),
863 })
864 .with_source("query_root");
865
866 assert_eq!(issue.severity, Severity::Warning);
867 assert_eq!(issue.suggested_fixes.len(), 1);
868 assert!(issue.source.is_some());
869 }
870
871 #[test]
872 fn test_severity_ordering() {
873 assert!(Severity::Info < Severity::Warning);
874 assert!(Severity::Warning < Severity::Error);
875 assert!(Severity::Error < Severity::Critical);
876 }
877
878 #[test]
879 fn test_reflection_report_acceptable() {
880 let query = create_test_query();
881 let result = QueryResult::empty();
882 let mut report = ReflectionReport::new(query, result);
883
884 report.quality_score = 0.7;
885 assert!(report.is_acceptable());
886
887 report.quality_score = 0.3;
888 assert!(!report.is_acceptable());
889
890 report.quality_score = 0.7;
891 report
892 .issues
893 .push(Issue::new(ErrorType::EmptyResult, Severity::Error, "Error"));
894 assert!(!report.is_acceptable());
895 }
896
897 #[test]
898 fn test_suggested_fix_description() {
899 let fix = SuggestedFix::ResolveEntity {
900 original: "main".to_string(),
901 suggested: "main.rs".to_string(),
902 };
903
904 let desc = fix.description();
905 assert!(desc.contains("main"));
906 assert!(desc.contains("main.rs"));
907 }
908
909 #[test]
910 fn test_error_type_description() {
911 let error = ErrorType::EntityNotFound("test.rs".to_string());
912 let desc = error.description();
913 assert!(desc.contains("test.rs"));
914 }
915
916 #[test]
917 fn test_substitute_entity() {
918 let reflection = ReflectionModule::new(ReflectionConfig::default());
919
920 let query = QueryCore::new(
921 QuestionType::Definition,
922 QueryExpr::constant("main", EntityType::File),
923 vec![("main".to_string(), EntityType::File)],
924 "What is main?".to_string(),
925 );
926
927 let corrected = reflection.substitute_entity(&query, "main", "main.rs");
928 assert!(corrected.is_some());
929
930 let corrected = corrected.unwrap();
931 assert!(corrected.entities.iter().any(|(name, _)| name == "main.rs"));
932 }
933
934 #[test]
935 fn test_correction_success_rate() {
936 let mut reflection = ReflectionModule::new(ReflectionConfig::default());
937
938 assert_eq!(reflection.correction_success_rate(), 0.0);
939
940 reflection.record_correction(
941 Issue::new(ErrorType::EmptyResult, Severity::Warning, "test"),
942 SuggestedFix::ExpandScope {
943 relation: "All".to_string(),
944 },
945 true,
946 );
947
948 reflection.record_correction(
949 Issue::new(ErrorType::EmptyResult, Severity::Warning, "test"),
950 SuggestedFix::ExpandScope {
951 relation: "All".to_string(),
952 },
953 false,
954 );
955
956 assert_eq!(reflection.correction_success_rate(), 0.5);
957 }
958}