use super::learning::LearningCoordinator;
use super::query_core::{QueryCore, QueryExpr, QueryOp, QueryResult, RelationType};
use brainwires_core::graph::RelationshipGraphT;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ErrorType {
EmptyResult,
ResultOverflow,
EntityNotFound(String),
RelationMismatch(String),
CoreferenceFailure(String),
SchemaAlignment(String),
Timeout,
Unknown(String),
}
impl ErrorType {
pub fn description(&self) -> String {
match self {
ErrorType::EmptyResult => "Query returned no results".to_string(),
ErrorType::ResultOverflow => "Query returned too many results".to_string(),
ErrorType::EntityNotFound(name) => format!("Entity '{}' not found", name),
ErrorType::RelationMismatch(rel) => format!("Relationship '{}' does not apply", rel),
ErrorType::CoreferenceFailure(ref_text) => {
format!("Could not resolve reference '{}'", ref_text)
}
ErrorType::SchemaAlignment(msg) => format!("Schema alignment issue: {}", msg),
ErrorType::Timeout => "Query execution timed out".to_string(),
ErrorType::Unknown(msg) => format!("Unknown error: {}", msg),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
impl Severity {
pub fn as_str(&self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "error",
Severity::Critical => "critical",
}
}
}
#[derive(Debug, Clone)]
pub struct Issue {
pub error_type: ErrorType,
pub severity: Severity,
pub message: String,
pub suggested_fixes: Vec<SuggestedFix>,
pub source: Option<String>,
}
impl Issue {
pub fn new(error_type: ErrorType, severity: Severity, message: &str) -> Self {
Self {
error_type,
severity,
message: message.to_string(),
suggested_fixes: Vec::new(),
source: None,
}
}
pub fn with_fix(mut self, fix: SuggestedFix) -> Self {
self.suggested_fixes.push(fix);
self
}
pub fn with_source(mut self, source: &str) -> Self {
self.source = Some(source.to_string());
self
}
}
#[derive(Debug, Clone)]
pub enum SuggestedFix {
RetryWithQuery(QueryCore),
ExpandScope {
relation: String,
},
NarrowScope {
filter: String,
},
ResolveEntity {
original: String,
suggested: String,
},
AddRelation {
from: String,
to: String,
relation: String,
},
ManualIntervention(String),
}
impl SuggestedFix {
pub fn description(&self) -> String {
match self {
SuggestedFix::RetryWithQuery(_) => "Retry with modified query".to_string(),
SuggestedFix::ExpandScope { relation } => {
format!("Expand scope to include {} relationships", relation)
}
SuggestedFix::NarrowScope { filter } => {
format!("Narrow scope with filter: {}", filter)
}
SuggestedFix::ResolveEntity {
original,
suggested,
} => {
format!("Resolve '{}' as '{}'", original, suggested)
}
SuggestedFix::AddRelation { from, to, relation } => {
format!("Add {} relationship from {} to {}", relation, from, to)
}
SuggestedFix::ManualIntervention(msg) => format!("Manual intervention: {}", msg),
}
}
}
#[derive(Debug, Clone)]
pub struct CorrectionRecord {
pub issue: Issue,
pub fix_applied: SuggestedFix,
pub success: bool,
pub timestamp: i64,
}
#[derive(Debug, Clone)]
pub struct ReflectionReport {
pub query: QueryCore,
pub result: QueryResult,
pub issues: Vec<Issue>,
pub quality_score: f32,
pub correction_attempted: bool,
pub corrected_query: Option<QueryCore>,
pub corrected_result: Option<QueryResult>,
}
impl ReflectionReport {
pub fn new(query: QueryCore, result: QueryResult) -> Self {
Self {
query,
result,
issues: Vec::new(),
quality_score: 1.0,
correction_attempted: false,
corrected_query: None,
corrected_result: None,
}
}
pub fn is_acceptable(&self) -> bool {
self.quality_score >= 0.5 && !self.issues.iter().any(|i| i.severity >= Severity::Error)
}
pub fn max_severity(&self) -> Option<Severity> {
self.issues.iter().map(|i| i.severity).max()
}
}
#[derive(Debug, Clone)]
pub struct ReflectionConfig {
pub max_results: usize,
pub min_results: usize,
pub max_retries: u32,
pub auto_correct: bool,
}
impl Default for ReflectionConfig {
fn default() -> Self {
Self {
max_results: 100,
min_results: 1,
max_retries: 2,
auto_correct: true,
}
}
}
pub struct ReflectionModule {
config: ReflectionConfig,
error_patterns: HashMap<ErrorType, u32>,
correction_history: Vec<CorrectionRecord>,
}
impl ReflectionModule {
pub fn new(config: ReflectionConfig) -> Self {
Self {
config,
error_patterns: HashMap::new(),
correction_history: Vec::new(),
}
}
pub fn analyze(
&mut self,
query: &QueryCore,
result: &QueryResult,
graph: &dyn RelationshipGraphT,
) -> ReflectionReport {
let mut report = ReflectionReport::new(query.clone(), result.clone());
if let Some(ref error) = result.error {
report.issues.push(Issue::new(
ErrorType::Unknown(error.clone()),
Severity::Error,
error,
));
report.quality_score = 0.0;
return report;
}
if result.values.is_empty() && result.count != Some(0) {
let issue = self.analyze_empty_result(query, graph);
report.issues.push(issue);
report.quality_score = 0.3;
}
if result.values.len() > self.config.max_results {
let issue = Issue::new(
ErrorType::ResultOverflow,
Severity::Warning,
&format!(
"Query returned {} results (max: {})",
result.values.len(),
self.config.max_results
),
)
.with_fix(SuggestedFix::NarrowScope {
filter: "Add type or name filter".to_string(),
});
report.issues.push(issue);
report.quality_score = 0.6;
}
for (entity_name, _entity_type) in &query.entities {
if graph.get_node(entity_name).is_none() {
let similar = self.find_similar_entities(entity_name, graph);
let mut issue = Issue::new(
ErrorType::EntityNotFound(entity_name.clone()),
Severity::Warning,
&format!("Entity '{}' not found in graph", entity_name),
);
if let Some(suggestion) = similar.first() {
issue = issue.with_fix(SuggestedFix::ResolveEntity {
original: entity_name.clone(),
suggested: suggestion.clone(),
});
}
report.issues.push(issue);
report.quality_score = (report.quality_score - 0.2).max(0.0);
}
}
self.validate_relationships(query, graph, &mut report);
for issue in &report.issues {
*self
.error_patterns
.entry(issue.error_type.clone())
.or_insert(0) += 1;
}
report
}
fn analyze_empty_result(&self, query: &QueryCore, graph: &dyn RelationshipGraphT) -> Issue {
for (entity_name, _) in &query.entities {
if graph.get_node(entity_name).is_none() {
return Issue::new(
ErrorType::EntityNotFound(entity_name.clone()),
Severity::Error,
&format!(
"Entity '{}' not found - query cannot return results",
entity_name
),
);
}
}
if let Some(relation_msg) = self.check_relationship_applicability(&query.root, graph) {
return Issue::new(
ErrorType::RelationMismatch(relation_msg.clone()),
Severity::Error,
&relation_msg,
)
.with_fix(SuggestedFix::ExpandScope {
relation: "CoOccurs".to_string(),
});
}
Issue::new(
ErrorType::EmptyResult,
Severity::Warning,
"Query returned no results",
)
.with_fix(SuggestedFix::ExpandScope {
relation: "All".to_string(),
})
}
fn check_relationship_applicability(
&self,
expr: &QueryExpr,
graph: &dyn RelationshipGraphT,
) -> Option<String> {
match expr {
QueryExpr::Op(QueryOp::Join {
relation,
subject,
object,
}) => {
let subject_name = match subject.as_ref() {
QueryExpr::Constant(name, _) => Some(name.as_str()),
_ => None,
};
let object_name = match object.as_ref() {
QueryExpr::Constant(name, _) => Some(name.as_str()),
_ => None,
};
if let Some(name) = subject_name.or(object_name) {
let edges = graph.get_edges(name);
if let Some(edge_type) = relation.to_edge_type()
&& !edges.iter().any(|e| e.edge_type == edge_type)
{
return Some(format!(
"No {:?} relationships found for '{}'",
relation, name
));
}
}
None
}
_ => None,
}
}
fn find_similar_entities(&self, name: &str, graph: &dyn RelationshipGraphT) -> Vec<String> {
let candidates = graph.search(name, 5);
candidates
.iter()
.map(|node| node.entity_name.clone())
.collect()
}
fn validate_relationships(
&self,
query: &QueryCore,
graph: &dyn RelationshipGraphT,
report: &mut ReflectionReport,
) {
self.validate_expr(&query.root, graph, report);
}
#[allow(clippy::only_used_in_recursion)]
fn validate_expr(
&self,
expr: &QueryExpr,
graph: &dyn RelationshipGraphT,
report: &mut ReflectionReport,
) {
match expr {
QueryExpr::Op(QueryOp::Join {
relation,
subject,
object,
}) => {
if relation.to_edge_type().is_none()
&& !matches!(
relation,
RelationType::HasType
| RelationType::HasError
| RelationType::CreatedAt
| RelationType::ModifiedAt
)
&& let RelationType::Custom(name) = relation
{
report.issues.push(
Issue::new(
ErrorType::RelationMismatch(name.clone()),
Severity::Warning,
&format!("Custom relationship '{}' may not exist", name),
)
.with_source(&format!("{:?}", relation)),
);
}
self.validate_expr(subject, graph, report);
self.validate_expr(object, graph, report);
}
QueryExpr::Op(QueryOp::And(exprs)) | QueryExpr::Op(QueryOp::Or(exprs)) => {
for e in exprs {
self.validate_expr(e, graph, report);
}
}
QueryExpr::Op(QueryOp::Filter { source, .. }) => {
self.validate_expr(source, graph, report);
}
QueryExpr::Op(QueryOp::Count(inner)) => {
self.validate_expr(inner, graph, report);
}
QueryExpr::Op(QueryOp::Superlative { source, .. }) => {
self.validate_expr(source, graph, report);
}
_ => {}
}
}
pub fn validate_query_core(&self, query: &QueryCore) -> Vec<Issue> {
let mut issues = Vec::new();
if query.entities.is_empty() {
issues.push(Issue::new(
ErrorType::SchemaAlignment("No entities in query".to_string()),
Severity::Warning,
"Query does not reference any entities",
));
}
if matches!(
query.question_type,
super::query_core::QuestionType::Unknown
) {
issues.push(Issue::new(
ErrorType::SchemaAlignment("Unknown question type".to_string()),
Severity::Info,
"Could not determine question type",
));
}
issues
}
pub fn attempt_correction(
&mut self,
report: &mut ReflectionReport,
graph: &dyn RelationshipGraphT,
_executor: &super::query_core::QueryExecutor,
) -> bool {
if !self.config.auto_correct {
return false;
}
if report.issues.is_empty() {
return true; }
report.correction_attempted = true;
for issue in &report.issues {
if issue.severity < Severity::Warning {
continue;
}
for fix in &issue.suggested_fixes {
match fix {
SuggestedFix::ResolveEntity {
original,
suggested,
} => {
if graph.get_node(suggested).is_some() {
let corrected =
self.substitute_entity(&report.query, original, suggested);
if let Some(corrected) = corrected {
report.corrected_query = Some(corrected);
self.record_correction(issue.clone(), fix.clone(), true);
return true;
}
}
}
SuggestedFix::ExpandScope { .. } => {
}
_ => {}
}
}
}
false
}
fn substitute_entity(
&self,
query: &QueryCore,
original: &str,
replacement: &str,
) -> Option<QueryCore> {
let mut corrected = query.clone();
for (name, _) in &mut corrected.entities {
if name == original {
*name = replacement.to_string();
}
}
corrected.root = Self::substitute_in_expr(&query.root, original, replacement);
Some(corrected)
}
fn substitute_in_expr(expr: &QueryExpr, original: &str, replacement: &str) -> QueryExpr {
match expr {
QueryExpr::Constant(name, entity_type) => {
if name == original {
QueryExpr::Constant(replacement.to_string(), entity_type.clone())
} else {
expr.clone()
}
}
QueryExpr::Op(op) => QueryExpr::Op(match op {
QueryOp::Join {
relation,
subject,
object,
} => QueryOp::Join {
relation: relation.clone(),
subject: Box::new(Self::substitute_in_expr(subject, original, replacement)),
object: Box::new(Self::substitute_in_expr(object, original, replacement)),
},
QueryOp::And(exprs) => QueryOp::And(
exprs
.iter()
.map(|e| Self::substitute_in_expr(e, original, replacement))
.collect(),
),
QueryOp::Or(exprs) => QueryOp::Or(
exprs
.iter()
.map(|e| Self::substitute_in_expr(e, original, replacement))
.collect(),
),
QueryOp::Filter { source, predicate } => QueryOp::Filter {
source: Box::new(Self::substitute_in_expr(source, original, replacement)),
predicate: predicate.clone(),
},
QueryOp::Count(inner) => QueryOp::Count(Box::new(Self::substitute_in_expr(
inner,
original,
replacement,
))),
QueryOp::Superlative {
source,
property,
direction,
} => QueryOp::Superlative {
source: Box::new(Self::substitute_in_expr(source, original, replacement)),
property: property.clone(),
direction: direction.clone(),
},
_ => op.clone(),
}),
_ => expr.clone(),
}
}
fn record_correction(&mut self, issue: Issue, fix: SuggestedFix, success: bool) {
self.correction_history.push(CorrectionRecord {
issue,
fix_applied: fix,
success,
timestamp: chrono::Utc::now().timestamp(),
});
}
pub fn provide_feedback(
&self,
report: &ReflectionReport,
coordinator: &mut LearningCoordinator,
) {
let success = report.is_acceptable();
let result_count = report.result.values.len();
coordinator.record_outcome(
None, success,
result_count,
Some(&report.query),
0, );
for issue in &report.issues {
if issue.severity >= Severity::Error {
}
}
}
pub fn get_error_stats(&self) -> HashMap<ErrorType, u32> {
self.error_patterns.clone()
}
pub fn correction_success_rate(&self) -> f32 {
if self.correction_history.is_empty() {
return 0.0;
}
let successes = self.correction_history.iter().filter(|r| r.success).count();
successes as f32 / self.correction_history.len() as f32
}
}
impl Default for ReflectionModule {
fn default() -> Self {
Self::new(ReflectionConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::seal::query_core::{QueryExpr, QueryResultValue, QuestionType};
use brainwires_core::graph::EntityType;
use brainwires_knowledge::RelationshipGraph;
fn create_test_query() -> QueryCore {
QueryCore::new(
QuestionType::Definition,
QueryExpr::var("x"),
vec![("main.rs".to_string(), EntityType::File)],
"What is main.rs?".to_string(),
)
}
#[test]
fn test_analyze_empty_result() {
let mut reflection = ReflectionModule::new(ReflectionConfig::default());
let query = create_test_query();
let result = QueryResult::empty();
let graph = RelationshipGraph::new();
let report = reflection.analyze(&query, &result, &graph);
assert!(!report.issues.is_empty());
assert!(report.issues.iter().any(|i| matches!(
i.error_type,
ErrorType::EmptyResult | ErrorType::EntityNotFound(_)
)));
}
#[test]
fn test_analyze_overflow_result() {
let mut reflection = ReflectionModule::new(ReflectionConfig {
max_results: 10,
..Default::default()
});
let query = create_test_query();
let mut values = Vec::new();
for i in 0..20 {
values.push(QueryResultValue {
value: format!("entity_{}", i),
entity_type: Some(EntityType::File),
score: 0.8,
metadata: std::collections::HashMap::new(),
});
}
let result = crate::seal::query_core::QueryResult::with_values(values);
let graph = RelationshipGraph::new();
let report = reflection.analyze(&query, &result, &graph);
assert!(
report
.issues
.iter()
.any(|i| i.error_type == ErrorType::ResultOverflow)
);
}
#[test]
fn test_validate_query_core() {
let reflection = ReflectionModule::new(ReflectionConfig::default());
let query = QueryCore::new(
QuestionType::Unknown,
QueryExpr::var("x"),
vec![],
"Test".to_string(),
);
let issues = reflection.validate_query_core(&query);
assert!(!issues.is_empty());
}
#[test]
fn test_issue_creation() {
let issue = Issue::new(ErrorType::EmptyResult, Severity::Warning, "No results")
.with_fix(SuggestedFix::ExpandScope {
relation: "All".to_string(),
})
.with_source("query_root");
assert_eq!(issue.severity, Severity::Warning);
assert_eq!(issue.suggested_fixes.len(), 1);
assert!(issue.source.is_some());
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
assert!(Severity::Error < Severity::Critical);
}
#[test]
fn test_reflection_report_acceptable() {
let query = create_test_query();
let result = QueryResult::empty();
let mut report = ReflectionReport::new(query, result);
report.quality_score = 0.7;
assert!(report.is_acceptable());
report.quality_score = 0.3;
assert!(!report.is_acceptable());
report.quality_score = 0.7;
report
.issues
.push(Issue::new(ErrorType::EmptyResult, Severity::Error, "Error"));
assert!(!report.is_acceptable());
}
#[test]
fn test_suggested_fix_description() {
let fix = SuggestedFix::ResolveEntity {
original: "main".to_string(),
suggested: "main.rs".to_string(),
};
let desc = fix.description();
assert!(desc.contains("main"));
assert!(desc.contains("main.rs"));
}
#[test]
fn test_error_type_description() {
let error = ErrorType::EntityNotFound("test.rs".to_string());
let desc = error.description();
assert!(desc.contains("test.rs"));
}
#[test]
fn test_substitute_entity() {
let reflection = ReflectionModule::new(ReflectionConfig::default());
let query = QueryCore::new(
QuestionType::Definition,
QueryExpr::constant("main", EntityType::File),
vec![("main".to_string(), EntityType::File)],
"What is main?".to_string(),
);
let corrected = reflection.substitute_entity(&query, "main", "main.rs");
assert!(corrected.is_some());
let corrected = corrected.unwrap();
assert!(corrected.entities.iter().any(|(name, _)| name == "main.rs"));
}
#[test]
fn test_correction_success_rate() {
let mut reflection = ReflectionModule::new(ReflectionConfig::default());
assert_eq!(reflection.correction_success_rate(), 0.0);
reflection.record_correction(
Issue::new(ErrorType::EmptyResult, Severity::Warning, "test"),
SuggestedFix::ExpandScope {
relation: "All".to_string(),
},
true,
);
reflection.record_correction(
Issue::new(ErrorType::EmptyResult, Severity::Warning, "test"),
SuggestedFix::ExpandScope {
relation: "All".to_string(),
},
false,
);
assert_eq!(reflection.correction_success_rate(), 0.5);
}
}