use std::collections::HashMap;
use std::sync::Arc;
use chrono::{DateTime, NaiveDate, Utc};
use parking_lot::Mutex;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::persist::PersistError as EngineError;
use crate::weights::WEIGHT_FLOOR;
const WEIGHT_EVENTS_COUNT_BY_DAY_SQL: &str =
include_str!("../../sql/weight_events_count_by_day.sql");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RankHistoryPoint {
pub ts: DateTime<Utc>,
pub weight_after: f32,
pub delta: f32,
pub channel: String,
pub context_id: Option<String>,
pub event_id: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffReport {
pub jaccard: f32,
pub added: Vec<Uuid>,
pub dropped: Vec<Uuid>,
pub rank_deltas: Vec<(Uuid, i32)>,
pub top_k_at_t1: Vec<Uuid>,
pub top_k_at_t2: Vec<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegressionReport {
pub event_id: Uuid,
pub query_text: String,
pub original_top_atoms: Vec<Uuid>,
pub current_top_atoms: Vec<Uuid>,
pub jaccard: f32,
pub added: Vec<Uuid>,
pub dropped: Vec<Uuid>,
pub timestamp_original: DateTime<Utc>,
}
pub async fn weights_as_of(
conn: &Arc<Mutex<Connection>>,
namespace: &str,
at_time: DateTime<Utc>,
) -> Result<HashMap<Uuid, f32>, EngineError> {
let conn = Arc::clone(conn);
let namespace_str = namespace.to_string();
let at_time_us = at_time.timestamp_micros();
tokio::task::spawn_blocking(move || {
let conn = conn.lock();
let mut result = HashMap::new();
let mut stmt = conn
.prepare(include_str!("../../sql/weight_events_latest_as_of.sql"))
.map_err(|e| EngineError::Internal(format!("weights_as_of prepare: {e}")))?;
let mut rows = stmt
.query(params![namespace_str, at_time_us])
.map_err(|e| EngineError::Internal(format!("weights_as_of query: {e}")))?;
while let Some(row) = rows
.next()
.map_err(|e| EngineError::Internal(format!("weights_as_of row: {e}")))?
{
let atom_id_str: String = row
.get(0)
.map_err(|e| EngineError::Internal(format!("weights_as_of col 0: {e}")))?;
let weight_after: f64 = row
.get(1)
.map_err(|e| EngineError::Internal(format!("weights_as_of col 1: {e}")))?;
if let Ok(uuid) = atom_id_str.parse::<Uuid>() {
let clamped =
(weight_after as f32).clamp(WEIGHT_FLOOR, crate::weights::WEIGHT_CEIL);
result.insert(uuid, clamped);
}
}
Ok(result)
})
.await
.map_err(|e| EngineError::Internal(format!("weights_as_of join: {e}")))?
}
#[cfg(test)]
fn compute_diff_report(top_k_at_t1: Vec<Uuid>, top_k_at_t2: Vec<Uuid>) -> DiffReport {
use std::collections::HashSet;
let set_t1: HashSet<Uuid> = top_k_at_t1.iter().copied().collect();
let set_t2: HashSet<Uuid> = top_k_at_t2.iter().copied().collect();
let intersection_size = set_t1.intersection(&set_t2).count();
let union_size = set_t1.union(&set_t2).count();
let jaccard = if union_size == 0 {
1.0_f32
} else {
intersection_size as f32 / union_size as f32
};
let added: Vec<Uuid> = set_t2.difference(&set_t1).copied().collect();
let dropped: Vec<Uuid> = set_t1.difference(&set_t2).copied().collect();
let rank_t1: HashMap<Uuid, usize> = top_k_at_t1
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let rank_t2: HashMap<Uuid, usize> = top_k_at_t2
.iter()
.enumerate()
.map(|(i, &id)| (id, i))
.collect();
let rank_deltas: Vec<(Uuid, i32)> = set_t1
.intersection(&set_t2)
.filter_map(|&id| {
let r1 = *rank_t1.get(&id)?;
let r2 = *rank_t2.get(&id)?;
Some((id, r2 as i32 - r1 as i32))
})
.collect();
DiffReport {
jaccard,
added,
dropped,
rank_deltas,
top_k_at_t1,
top_k_at_t2,
}
}
pub async fn rank_history(
conn: &Arc<Mutex<Connection>>,
namespace: &str,
atom_id: Uuid,
) -> Result<Vec<RankHistoryPoint>, EngineError> {
let conn = Arc::clone(conn);
let namespace_str = namespace.to_string();
let atom_id_str = atom_id.to_string();
tokio::task::spawn_blocking(move || {
let conn = conn.lock();
let mut stmt = conn
.prepare(include_str!("../../sql/weight_events_history_by_atom.sql"))
.map_err(|e| EngineError::Internal(format!("rank_history prepare: {e}")))?;
let rows = stmt
.query_map(params![namespace_str, atom_id_str], |row| {
let ts_us: i64 = row.get(0)?;
let weight_after: f64 = row.get(1)?;
let delta: f64 = row.get(2)?;
let channel: String = row.get(3)?;
let context_id: Option<String> = row.get(4)?;
let event_id_str: Option<String> = row.get(5)?;
Ok((
ts_us,
weight_after,
delta,
channel,
context_id,
event_id_str,
))
})
.map_err(|e| EngineError::Internal(format!("rank_history query: {e}")))?;
let mut points = Vec::new();
for row in rows {
let (ts_us, weight_after, delta, channel, context_id, event_id_str) =
row.map_err(|e| EngineError::Internal(format!("rank_history row: {e}")))?;
let ts = DateTime::from_timestamp_micros(ts_us).unwrap_or_else(Utc::now);
let event_id = event_id_str.and_then(|s| s.parse::<Uuid>().ok());
points.push(RankHistoryPoint {
ts,
weight_after: weight_after as f32,
delta: delta as f32,
channel,
context_id,
event_id,
});
}
Ok(points)
})
.await
.map_err(|e| EngineError::Internal(format!("rank_history join: {e}")))?
}
pub mod metrics {
use super::*;
pub async fn atom_rank_variance(
conn: &Arc<Mutex<Connection>>,
namespace: &str,
atom_id: Uuid,
) -> Result<f32, EngineError> {
let conn = Arc::clone(conn);
let atom_id_str = atom_id.to_string();
let namespace_str = namespace.to_string();
tokio::task::spawn_blocking(move || {
let c = conn.lock();
let mut stmt = c
.prepare("SELECT payload FROM brain_events WHERE kind = 'ComposeEvent' AND json_extract(payload, '$.lambda_id') = ?1")
.map_err(|e| EngineError::Internal(format!("atom_rank_variance prepare: {e}")))?;
let rows = stmt
.query_map(params![namespace_str], |row| row.get::<_, String>(0))
.map_err(|e| EngineError::Internal(format!("atom_rank_variance query: {e}")))?;
let mut ranks: Vec<f32> = Vec::new();
for row in rows.filter_map(|r| r.ok()) {
let payload: serde_json::Value =
serde_json::from_str(&row).unwrap_or(serde_json::json!({}));
if let Some(top_atoms) = payload.get("top_atoms").and_then(|v| v.as_array()) {
if let Some(pos) = top_atoms
.iter()
.position(|v| v.as_str() == Some(&atom_id_str))
{
ranks.push(pos as f32);
}
}
}
if ranks.len() < 2 {
return Ok(0.0_f32);
}
let mean = ranks.iter().sum::<f32>() / ranks.len() as f32;
let variance =
ranks.iter().map(|r| (r - mean).powi(2)).sum::<f32>() / ranks.len() as f32;
Ok(variance)
})
.await
.map_err(|e| EngineError::Internal(format!("atom_rank_variance join: {e}")))?
}
pub async fn adjustment_rate_per_day(
conn: &Arc<Mutex<Connection>>,
namespace: &str,
days: u32,
) -> Result<Vec<(NaiveDate, u64)>, EngineError> {
let conn = Arc::clone(conn);
let namespace_str = namespace.to_string();
let days_i64 = days as i64;
tokio::task::spawn_blocking(move || {
let c = conn.lock();
let cutoff_us = (Utc::now() - chrono::Duration::days(days_i64)).timestamp_micros();
let mut stmt = c.prepare(WEIGHT_EVENTS_COUNT_BY_DAY_SQL).map_err(|e| {
EngineError::Internal(format!("adjustment_rate_per_day prepare: {e}"))
})?;
let rows = stmt
.query_map(params![namespace_str, cutoff_us], |row| {
let day_bucket: i64 = row.get(0)?;
let cnt: i64 = row.get(1)?;
Ok((day_bucket, cnt as u64))
})
.map_err(|e| {
EngineError::Internal(format!("adjustment_rate_per_day query: {e}"))
})?;
let mut result = Vec::new();
for row in rows.filter_map(|r| r.ok()) {
let (day_bucket, cnt) = row;
const UNIX_EPOCH_CE_DAYS: i32 = 719_163;
let date =
NaiveDate::from_num_days_from_ce_opt(UNIX_EPOCH_CE_DAYS + day_bucket as i32)
.unwrap_or(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap());
result.push((date, cnt));
}
Ok(result)
})
.await
.map_err(|e| EngineError::Internal(format!("adjustment_rate_per_day join: {e}")))?
}
}
#[cfg(test)]
#[path = "engine_replay/tests.rs"]
mod tests;