use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use mongreldb_core::RowId;
use mongreldb_types::ids::TabletId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocalCandidate {
pub tablet_id: TabletId,
pub row_id: RowId,
pub score: f64,
pub local_rank: u32,
pub rls_visible: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FusionMethod {
Rrf {
k: u32,
},
MaxScore,
}
impl Default for FusionMethod {
fn default() -> Self {
Self::Rrf { k: 60 }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiWorkBudget {
pub candidate_ceiling: usize,
pub max_local_candidates: usize,
pub memory_bytes: u64,
pub deadline_ms: u64,
}
impl Default for AiWorkBudget {
fn default() -> Self {
Self {
candidate_ceiling: 100,
max_local_candidates: 1_000,
memory_bytes: 64 * 1024 * 1024,
deadline_ms: 5_000,
}
}
}
pub fn adaptive_local_k(global_k: usize, overfetch_factor: f64, active_tablets: usize) -> usize {
let tablets = active_tablets.max(1) as f64;
let raw = (global_k as f64) * overfetch_factor / tablets;
raw.ceil().max(1.0) as usize
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum AiRetrievalError {
#[error("RLS hygiene violation: tablet {tablet_id} emitted hidden row {row_id}")]
RlsHygiene {
tablet_id: TabletId,
row_id: u64,
},
#[error("AI work budget exceeded: {0}")]
BudgetExceeded(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MergedCandidate {
pub tablet_id: TabletId,
pub row_id: RowId,
pub final_score: f64,
pub raw_score: f64,
}
#[derive(Debug, Clone)]
struct HeapKey {
final_score: f64,
tablet_id: TabletId,
row_id: RowId,
}
impl PartialEq for HeapKey {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for HeapKey {}
impl PartialOrd for HeapKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HeapKey {
fn cmp(&self, other: &Self) -> Ordering {
match self
.final_score
.partial_cmp(&other.final_score)
.unwrap_or(Ordering::Equal)
{
Ordering::Equal => other
.tablet_id
.cmp(&self.tablet_id)
.then_with(|| other.row_id.cmp(&self.row_id)),
ord => ord,
}
}
}
pub fn merge_candidates(
locals: &[LocalCandidate],
method: FusionMethod,
budget: &AiWorkBudget,
) -> Result<Vec<MergedCandidate>, AiRetrievalError> {
if locals.len() > budget.max_local_candidates {
return Err(AiRetrievalError::BudgetExceeded(format!(
"{} local candidates > max {}",
locals.len(),
budget.max_local_candidates
)));
}
for c in locals {
if !c.rls_visible {
return Err(AiRetrievalError::RlsHygiene {
tablet_id: c.tablet_id,
row_id: c.row_id.0,
});
}
}
let mut by_key: HashMap<(TabletId, RowId), LocalCandidate> = HashMap::new();
for c in locals {
by_key
.entry((c.tablet_id, c.row_id))
.and_modify(|existing| {
if c.score > existing.score {
*existing = c.clone();
}
})
.or_insert_with(|| c.clone());
}
let fused: Vec<MergedCandidate> = match method {
FusionMethod::Rrf { k } => {
by_key
.into_values()
.map(|c| {
let rrf = 1.0 / (f64::from(k) + f64::from(c.local_rank));
MergedCandidate {
tablet_id: c.tablet_id,
row_id: c.row_id,
final_score: rrf,
raw_score: c.score,
}
})
.collect()
}
FusionMethod::MaxScore => by_key
.into_values()
.map(|c| MergedCandidate {
tablet_id: c.tablet_id,
row_id: c.row_id,
final_score: c.score,
raw_score: c.score,
})
.collect(),
};
let mut heap: BinaryHeap<HeapKey> = BinaryHeap::new();
let mut map: HashMap<(TabletId, RowId), MergedCandidate> = HashMap::new();
for m in fused {
let key = (m.tablet_id, m.row_id);
heap.push(HeapKey {
final_score: m.final_score,
tablet_id: m.tablet_id,
row_id: m.row_id,
});
map.insert(key, m);
}
let mut out = Vec::with_capacity(budget.candidate_ceiling.min(map.len()));
while out.len() < budget.candidate_ceiling {
let Some(hk) = heap.pop() else {
break;
};
if let Some(m) = map.remove(&(hk.tablet_id, hk.row_id)) {
out.push(m);
}
}
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiConsistencyAudit {
pub read_ts: String,
pub replica_applied_ts: String,
pub staleness_micros: u64,
pub index_applied_ts: String,
pub model_version: Option<String>,
pub preprocessing_version: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn tid(n: u8) -> TabletId {
TabletId::from_bytes({
let mut b = [0u8; 16];
b[15] = n;
b
})
}
fn cand(tablet: u8, row: u64, score: f64, rank: u32) -> LocalCandidate {
LocalCandidate {
tablet_id: tid(tablet),
row_id: RowId(row),
score,
local_rank: rank,
rls_visible: true,
}
}
#[test]
fn merge_is_deterministic() {
let locals = vec![
cand(2, 10, 0.9, 1),
cand(1, 20, 0.8, 1),
cand(2, 30, 0.7, 2),
cand(1, 10, 0.95, 2),
];
let budget = AiWorkBudget {
candidate_ceiling: 10,
..AiWorkBudget::default()
};
let a = merge_candidates(&locals, FusionMethod::Rrf { k: 60 }, &budget).unwrap();
let b = merge_candidates(&locals, FusionMethod::Rrf { k: 60 }, &budget).unwrap();
assert_eq!(a, b);
assert!(a[0].final_score >= a[1].final_score);
}
#[test]
fn rls_hidden_row_fails_closed() {
let mut c = cand(1, 1, 1.0, 1);
c.rls_visible = false;
let err =
merge_candidates(&[c], FusionMethod::default(), &AiWorkBudget::default()).unwrap_err();
assert!(matches!(err, AiRetrievalError::RlsHygiene { .. }));
}
#[test]
fn adaptive_local_k_scales() {
assert_eq!(adaptive_local_k(10, 2.0, 5), 4); assert_eq!(adaptive_local_k(10, 2.0, 1), 20);
assert_eq!(adaptive_local_k(10, 2.0, 0), 20); }
#[test]
fn tie_break_tablet_then_row() {
let locals = vec![cand(2, 5, 1.0, 1), cand(1, 9, 1.0, 1)];
let out = merge_candidates(
&locals,
FusionMethod::Rrf { k: 60 },
&AiWorkBudget {
candidate_ceiling: 2,
..AiWorkBudget::default()
},
)
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].tablet_id, tid(1));
assert_eq!(out[1].tablet_id, tid(2));
}
}