use std::path::Path;
use anyhow::Result;
use patina::mother::{self, EdgeType, Graph};
use crate::commands::persona;
use super::super::{ScryOptions, ScryResult};
use super::enrichment::truncate_content;
use super::logging::{log_scry_query_with_routing, EdgeInfo, RoutedResult, RoutingContext};
use super::search::scry_text;
pub fn execute_via_mother(query: Option<&str>, options: &ScryOptions) -> Result<()> {
let address = mother::get_address().unwrap_or_else(|| "unknown".to_string());
println!("🔮 Scry - Querying mother at {}\n", address);
if options.file.is_some() {
anyhow::bail!("File-based queries (--file) not supported via mother. Run locally.");
}
let query = query.ok_or_else(|| anyhow::anyhow!("Query text required"))?;
println!("Query: \"{}\"\n", query);
let request = mother::ScryRequest {
query: query.to_string(),
dimension: options.dimension.clone(),
repo: options.repo.clone(),
all_repos: options.all_repos,
include_issues: options.include_issues,
include_persona: options.include_persona,
limit: options.limit,
min_score: options.min_score,
};
let response = mother::scry(request)?;
if response.results.is_empty() {
println!("No results found.");
return Ok(());
}
println!("Found {} results:\n", response.count);
println!("{}", "─".repeat(60));
for (i, result) in response.results.iter().enumerate() {
let timestamp_display = if result.timestamp.is_empty() {
String::new()
} else {
format!(" | {}", result.timestamp)
};
println!(
"\n[{}] Score: {:.3} | {} | {}{}",
i + 1,
result.score,
result.event_type,
result.source_id,
timestamp_display
);
println!(" {}", truncate_content(&result.content, 200));
}
println!("\n{}", "─".repeat(60));
Ok(())
}
pub fn execute_graph_routing(query: Option<&str>, options: &ScryOptions) -> Result<()> {
let query = query.ok_or_else(|| anyhow::anyhow!("Query required for graph routing"))?;
println!("Mode: Graph Routing (smart cross-project search)\n");
println!("Query: \"{}\"\n", query);
let graph = Graph::open()?;
let current_project = detect_current_project(&graph)?;
println!("📍 Current project: {}", current_project);
let edge_types = [EdgeType::Uses, EdgeType::TestsWith, EdgeType::LearnsFrom];
let related_nodes = graph.get_related(¤t_project, &edge_types)?;
if related_nodes.is_empty() {
println!("⚠️ No related repos in graph. Falling back to current project only.");
println!(" Tip: Use 'patina mother link' to add relationships.\n");
} else {
println!(
"🔗 Related repos: {}",
related_nodes
.iter()
.map(|n| n.id.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
let query_lower = query.to_lowercase();
let filtered_nodes: Vec<_> = if should_filter_by_domain(&query_lower) {
let filtered: Vec<_> = related_nodes
.iter()
.filter(|n| node_matches_query_domain(n, &query_lower))
.collect();
if !filtered.is_empty() && filtered.len() < related_nodes.len() {
println!(
"🎯 Domain filter: {} (matched {} of {} related)",
filtered
.iter()
.map(|n| n.id.as_str())
.collect::<Vec<_>>()
.join(", "),
filtered.len(),
related_nodes.len()
);
filtered.into_iter().cloned().collect()
} else {
related_nodes.clone()
}
} else {
related_nodes.clone()
};
let repos_to_search: Vec<String> = filtered_nodes.iter().map(|n| n.id.clone()).collect();
let edges = graph.get_edges_from(¤t_project)?;
let edges_used: Vec<EdgeInfo> = edges
.iter()
.filter(|e| repos_to_search.contains(&e.to_node))
.map(|e| EdgeInfo {
id: e.id,
from_node: e.from_node.clone(),
to_node: e.to_node.clone(),
edge_type: e.edge_type.as_str().to_string(),
weight: e.weight,
})
.collect();
let domain_filter_applied = filtered_nodes.len() < related_nodes.len();
println!();
let mut all_results: Vec<(String, String, f32, ScryResult)> = Vec::new();
let in_project = Path::new(".patina/local/data/patina.db").exists();
if in_project {
println!("📂 Searching current project...");
let project_options = ScryOptions {
repo: None,
all_repos: false,
..options.clone()
};
match scry_text(query, &project_options) {
Ok(results) => {
println!(" Found {} results", results.len());
for r in results {
all_results.push(("[PROJECT]".to_string(), current_project.clone(), 1.0, r));
}
}
Err(e) => {
eprintln!(" ⚠️ Project search failed: {}", e);
}
}
}
let repos_searched = repos_to_search.len();
for repo_id in &repos_to_search {
println!("📚 Searching {}...", repo_id);
let repo_options = ScryOptions {
repo: Some(repo_id.clone()),
all_repos: false,
..options.clone()
};
match scry_text(query, &repo_options) {
Ok(results) => {
println!(" Found {} results", results.len());
let weight = get_relationship_weight(&edges, repo_id);
for r in results {
all_results.push((
format!("[{}]", repo_id.to_uppercase()),
repo_id.clone(),
weight,
r,
));
}
}
Err(e) => {
eprintln!(" ⚠️ {} search failed: {}", repo_id, e);
}
}
}
if options.include_persona {
println!("🧠 Searching persona...");
if let Ok(persona_results) = persona::query(query, options.limit, options.min_score, None) {
println!(" Found {} results", persona_results.len());
for p in persona_results {
all_results.push((
"[PERSONA]".to_string(),
"persona".to_string(), 1.0, ScryResult {
id: 0,
content: p.content,
score: p.score,
event_type: p.source.clone(),
source_id: p.domains.join(", "),
timestamp: p.timestamp,
},
));
}
}
}
all_results.sort_by(|a, b| {
let weighted_a = a.3.score * a.2;
let weighted_b = b.3.score * b.2;
weighted_b
.partial_cmp(&weighted_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
all_results.truncate(options.limit);
let total_repos = crate::commands::repo::list().map(|r| r.len()).unwrap_or(0);
let routing_context = RoutingContext {
strategy: "graph".to_string(),
source_project: current_project.clone(),
edges_used,
repos_searched: repos_to_search.clone(),
repos_available: total_repos + 1, domain_filter_applied,
};
let routed_results: Vec<RoutedResult> = all_results
.iter()
.map(|(_, repo_id, weight, result)| RoutedResult {
source_repo: repo_id.clone(),
weight: *weight,
result: result.clone(),
})
.collect();
let query_id = log_scry_query_with_routing(query, &routed_results, &routing_context);
if let Some(ref qid) = query_id {
for edge in &routing_context.edges_used {
let best_rank = routed_results
.iter()
.enumerate()
.find(|(_, r)| r.source_repo == edge.to_node)
.map(|(i, _)| i + 1);
let _ = graph.record_edge_usage(edge.id, qid, &edge.to_node, best_rank);
}
}
println!();
if all_results.is_empty() {
println!("No results found.");
return Ok(());
}
println!(
"Found {} results (searched {} of {} repos):\n",
all_results.len(),
repos_searched + 1, total_repos + 1
);
println!("{}", "─".repeat(60));
for (i, (source, _repo_id, weight, result)) in all_results.iter().enumerate() {
let timestamp_display = if result.timestamp.is_empty() {
String::new()
} else {
format!(" | {}", result.timestamp)
};
let weight_display = if (*weight - 1.0).abs() > 0.01 {
format!(" (w={:.2})", weight)
} else {
String::new()
};
println!(
"\n[{}] {} Score: {:.3}{} | {} | {}{}",
i + 1,
source,
result.score,
weight_display,
result.event_type,
result.source_id,
timestamp_display
);
println!(" {}", truncate_content(&result.content, 200));
}
println!("\n{}", "─".repeat(60));
if let Some(ref qid) = query_id {
println!("\nQuery ID: {} (use with 'scry open/copy/feedback')", qid);
}
Ok(())
}
fn detect_current_project(graph: &Graph) -> Result<String> {
let cwd = std::env::current_dir()?;
let nodes = graph.list_nodes()?;
for node in &nodes {
if node.path == cwd {
return Ok(node.id.clone());
}
}
let dir_name = cwd
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
if graph.get_node(dir_name)?.is_some() {
return Ok(dir_name.to_string());
}
Ok(dir_name.to_string())
}
fn should_filter_by_domain(query: &str) -> bool {
let domain_hints = [
"cairo",
"rust",
"typescript",
"javascript",
"python",
"go",
"java",
"c++",
"cpp",
"solidity",
"prolog",
"dojo",
"starknet",
"mcp",
"ecs",
"vector",
"embedding",
];
domain_hints.iter().any(|hint| query.contains(hint))
}
fn node_matches_query_domain(node: &mother::Node, query: &str) -> bool {
for domain in &node.domains {
if query.contains(&domain.to_lowercase()) {
return true;
}
}
if query.contains(&node.id.to_lowercase()) {
return true;
}
false
}
fn get_relationship_weight(edges: &[mother::Edge], repo_id: &str) -> f32 {
for edge in edges {
if edge.to_node == repo_id {
return match edge.edge_type {
EdgeType::TestsWith => 1.2, EdgeType::LearnsFrom => 1.1, EdgeType::Uses => 1.1, EdgeType::Sibling => 1.0, EdgeType::Domain => 1.0, };
}
}
1.0 }