use std::sync::Mutex;
use anyhow::Result;
use rusqlite::Connection;
use patina::eventlog;
use super::super::ScryResult;
#[derive(Debug, Clone)]
pub struct EdgeInfo {
pub id: i64,
pub from_node: String,
pub to_node: String,
pub edge_type: String,
pub weight: f32,
}
#[derive(Debug, Clone, Default)]
pub struct RoutingContext {
pub strategy: String,
pub source_project: String,
pub edges_used: Vec<EdgeInfo>,
pub repos_searched: Vec<String>,
pub repos_available: usize,
pub domain_filter_applied: bool,
}
pub static LAST_QUERY_ID: Mutex<Option<String>> = Mutex::new(None);
pub fn generate_query_id() -> String {
let now = chrono::Utc::now();
let random_suffix: String = (0..3)
.map(|_| (b'a' + fastrand::u8(0..26)) as char)
.collect();
format!("q_{}_{}", now.format("%Y%m%d_%H%M%S"), random_suffix)
}
pub fn get_active_session_id() -> Option<String> {
let content = std::fs::read_to_string(".patina/local/active-session.md").ok()?;
if let Some(after_start) = content.strip_prefix("---") {
if let Some(end) = after_start.find("---") {
let frontmatter = &after_start[..end];
for line in frontmatter.lines() {
if let Some(value) = line.strip_prefix("id:") {
return Some(value.trim().to_string());
}
}
}
}
for line in content.lines() {
if line.starts_with("**ID**:") {
return Some(line.replace("**ID**:", "").trim().to_string());
}
}
None
}
pub fn log_scry_query(query: &str, mode: &str, results: &[ScryResult]) -> Option<String> {
let session_id = get_active_session_id()?;
let query_id = generate_query_id();
let results_json: Vec<serde_json::Value> = results
.iter()
.enumerate()
.map(|(i, r)| {
serde_json::json!({
"doc_id": r.source_id,
"score": r.score,
"rank": i + 1,
"event_type": r.event_type
})
})
.collect();
let query_data = serde_json::json!({
"query": query,
"query_id": query_id,
"mode": mode,
"session_id": session_id,
"results": results_json
});
let insert_result = (|| -> Result<()> {
let conn = Connection::open(eventlog::PATINA_DB)?;
let timestamp = chrono::Utc::now().to_rfc3339();
eventlog::insert_event(
&conn,
"scry.query",
×tamp,
&query_id, None,
&query_data.to_string(),
)?;
Ok(())
})();
if insert_result.is_ok() {
if let Ok(mut last) = LAST_QUERY_ID.lock() {
*last = Some(query_id.clone());
}
Some(query_id)
} else {
None
}
}
#[derive(Debug, Clone)]
pub struct RoutedResult {
pub source_repo: String,
pub weight: f32,
pub result: ScryResult,
}
pub fn log_scry_query_with_routing(
query: &str,
results: &[RoutedResult],
routing: &RoutingContext,
) -> Option<String> {
let session_id = get_active_session_id()?;
let query_id = generate_query_id();
let results_json: Vec<serde_json::Value> = results
.iter()
.enumerate()
.map(|(i, r)| {
serde_json::json!({
"doc_id": r.result.source_id,
"score": r.result.score,
"rank": i + 1,
"event_type": r.result.event_type,
"source_repo": r.source_repo,
"weight": r.weight
})
})
.collect();
let edges_json: Vec<serde_json::Value> = routing
.edges_used
.iter()
.map(|e| {
serde_json::json!({
"id": e.id,
"from": e.from_node,
"to": e.to_node,
"type": e.edge_type,
"weight": e.weight
})
})
.collect();
let query_data = serde_json::json!({
"query": query,
"query_id": query_id,
"mode": "graph",
"session_id": session_id,
"routing": {
"strategy": routing.strategy,
"source_project": routing.source_project,
"edges_used": edges_json,
"repos_searched": routing.repos_searched,
"repos_available": routing.repos_available,
"domain_filter_applied": routing.domain_filter_applied
},
"results": results_json
});
let insert_result = (|| -> Result<()> {
let conn = Connection::open(eventlog::PATINA_DB)?;
let timestamp = chrono::Utc::now().to_rfc3339();
eventlog::insert_event(
&conn,
"scry.query",
×tamp,
&query_id,
None,
&query_data.to_string(),
)?;
Ok(())
})();
if insert_result.is_ok() {
if let Ok(mut last) = LAST_QUERY_ID.lock() {
*last = Some(query_id.clone());
}
Some(query_id)
} else {
None
}
}
pub fn log_scry_use(query_id: &str, doc_id: &str, rank: usize) {
let session_id = get_active_session_id();
let use_data = serde_json::json!({
"query_id": query_id,
"result_used": doc_id,
"rank": rank,
"session_id": session_id
});
let _ = (|| -> Result<()> {
let conn = Connection::open(eventlog::PATINA_DB)?;
let timestamp = chrono::Utc::now().to_rfc3339();
eventlog::insert_event(
&conn,
"scry.use",
×tamp,
query_id,
None,
&use_data.to_string(),
)?;
Ok(())
})();
let _ = mark_edge_usage_from_query(query_id, rank);
}
fn mark_edge_usage_from_query(query_id: &str, rank: usize) -> Result<()> {
use patina::mother::Graph;
let conn = Connection::open(eventlog::PATINA_DB)?;
let data: String = conn.query_row(
"SELECT data FROM eventlog WHERE event_type = 'scry.query' AND source_id = ?",
[query_id],
|row| row.get(0),
)?;
let parsed: serde_json::Value = serde_json::from_str(&data)?;
if parsed.get("routing").is_none() {
return Ok(()); }
let results = parsed["results"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("No results in query"))?;
let result = results
.iter()
.find(|r| r["rank"].as_u64() == Some(rank as u64))
.ok_or_else(|| anyhow::anyhow!("Result not found at rank {}", rank))?;
let source_repo = result["source_repo"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No source_repo in result"))?;
let graph = Graph::open()?;
graph.mark_usage_useful(query_id, source_repo)?;
Ok(())
}
pub fn log_scry_feedback(query_id: &str, signal: &str, comment: Option<&str>) {
let session_id = get_active_session_id();
let feedback_data = serde_json::json!({
"query_id": query_id,
"signal": signal,
"comment": comment,
"session_id": session_id
});
let _ = (|| -> Result<()> {
let conn = Connection::open(eventlog::PATINA_DB)?;
let timestamp = chrono::Utc::now().to_rfc3339();
eventlog::insert_event(
&conn,
"scry.feedback",
×tamp,
query_id,
None,
&feedback_data.to_string(),
)?;
Ok(())
})();
}
pub fn get_query_results(query_id: &str) -> Result<Vec<(String, f32)>> {
let conn = Connection::open(eventlog::PATINA_DB)?;
let data: String = conn.query_row(
"SELECT data FROM eventlog WHERE event_type = 'scry.query' AND source_id = ?",
[query_id],
|row| row.get(0),
)?;
let parsed: serde_json::Value = serde_json::from_str(&data)?;
let results = parsed["results"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("No results in query"))?;
Ok(results
.iter()
.map(|r| {
(
r["doc_id"].as_str().unwrap_or("").to_string(),
r["score"].as_f64().unwrap_or(0.0) as f32,
)
})
.collect())
}