use crate::{DeviceId, DeviceStatus, MnemesError};
use semantic_memory::{SearchResult, SearchSourceType};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShardState {
Active,
Quarantined,
Revoked,
}
impl ShardState {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Quarantined => "quarantined",
Self::Revoked => "revoked",
}
}
pub(crate) fn parse(value: &str) -> Result<Self, MnemesError> {
match value {
"active" => Ok(Self::Active),
"quarantined" => Ok(Self::Quarantined),
"revoked" => Ok(Self::Revoked),
other => Err(MnemesError::InvalidShardCatalog(format!(
"invalid shard state '{other}'"
))),
}
}
}
impl From<DeviceStatus> for ShardState {
fn from(value: DeviceStatus) -> Self {
match value {
DeviceStatus::Active => Self::Active,
DeviceStatus::Quarantined => Self::Quarantined,
DeviceStatus::Revoked => Self::Revoked,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceShard {
pub device_id: DeviceId,
pub relative_path: PathBuf,
pub state: ShardState,
pub generation: u64,
pub routing_terms: String,
pub namespaces: Vec<String>,
pub fact_count: u64,
pub document_count: u64,
pub chunk_count: u64,
pub message_count: u64,
pub search_count: u64,
pub ewma_latency_ms: f64,
pub last_refreshed_at: Option<String>,
pub created_at: String,
pub device_status: DeviceStatus,
}
impl DeviceShard {
pub fn active(device_id: DeviceId, relative_path: impl Into<PathBuf>) -> Self {
Self {
device_id,
relative_path: relative_path.into(),
state: ShardState::Active,
generation: 1,
routing_terms: String::new(),
namespaces: Vec::new(),
fact_count: 0,
document_count: 0,
chunk_count: 0,
message_count: 0,
search_count: 0,
ewma_latency_ms: 0.0,
last_refreshed_at: None,
created_at: String::new(),
device_status: DeviceStatus::Active,
}
}
pub(crate) fn eligible(&self) -> bool {
self.state == ShardState::Active && self.device_status == DeviceStatus::Active
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RankedShard {
pub device_id: DeviceId,
pub score: u64,
}
pub fn routing_tokens(value: &str) -> Vec<String> {
let mut tokens = value
.split(|character: char| !character.is_ascii_alphanumeric())
.filter(|token| !token.is_empty())
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>();
tokens.sort();
tokens.dedup();
tokens
}
pub fn rank_shards(
query: &str,
requester_device_id: &DeviceId,
shards: &[DeviceShard],
) -> Vec<RankedShard> {
let query_terms = routing_tokens(query).into_iter().collect::<HashSet<_>>();
let mut ranked = shards
.iter()
.filter(|shard| shard.eligible())
.map(|shard| {
let mut catalog_terms = routing_tokens(&shard.routing_terms)
.into_iter()
.collect::<HashSet<_>>();
for namespace in &shard.namespaces {
catalog_terms.extend(routing_tokens(namespace));
}
let overlap = query_terms.intersection(&catalog_terms).count() as u64;
let locality = u64::from(&shard.device_id == requester_device_id);
RankedShard {
device_id: shard.device_id.clone(),
score: overlap.saturating_mul(2).saturating_add(locality),
}
})
.collect::<Vec<_>>();
ranked.sort_by(|left, right| {
right
.score
.cmp(&left.score)
.then_with(|| left.device_id.as_str().cmp(right.device_id.as_str()))
});
ranked
}
#[derive(Debug, Clone)]
pub struct RoutingSearchRequest {
pub query: String,
pub top_k: usize,
pub namespaces: Option<Vec<String>>,
pub source_types: Option<Vec<SearchSourceType>>,
pub shard_budget: Option<usize>,
pub exhaustive: bool,
}
impl RoutingSearchRequest {
pub fn new(query: impl Into<String>, top_k: usize) -> Self {
Self {
query: query.into(),
top_k: top_k.max(1),
namespaces: None,
source_types: None,
shard_budget: None,
exhaustive: false,
}
}
pub fn with_shard_budget(mut self, shard_budget: usize) -> Self {
self.shard_budget = Some(shard_budget.max(1));
self
}
pub fn exhaustive(mut self) -> Self {
self.exhaustive = true;
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct RoutedSearchResult {
pub result: SearchResult,
pub device_id: DeviceId,
pub shard_generation: u64,
pub child_receipt_id: Option<String>,
}
pub fn merge_routed_results(
mut results: Vec<RoutedSearchResult>,
top_k: usize,
) -> Result<Vec<RoutedSearchResult>, MnemesError> {
results.sort_by(|left, right| {
right
.result
.score
.total_cmp(&left.result.score)
.then_with(|| {
left.result
.source
.result_id()
.cmp(&right.result.source.result_id())
})
});
let mut seen = HashMap::<String, String>::new();
for result in &results {
let item_id = result.result.source.result_id();
if let Some(existing_content) = seen.get(&item_id) {
if existing_content != &result.result.content {
return Err(MnemesError::ConflictingShardItem { item_id });
}
} else {
seen.insert(item_id, result.result.content.clone());
}
}
let mut emitted = HashSet::<String>::new();
let mut merged = Vec::with_capacity(results.len().min(top_k));
for result in results {
let item_id = result.result.source.result_id();
if !emitted.insert(item_id) {
continue;
}
merged.push(result);
if merged.len() == top_k {
break;
}
}
Ok(merged)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardSearchOutcome {
pub device_id: DeviceId,
pub shard_generation: u64,
pub latency_ms: u64,
pub result_count: usize,
pub child_search_receipt_id: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardRoutingReceipt {
pub receipt_id: String,
pub requester_device_id: DeviceId,
pub query_sha256: String,
pub shard_budget: usize,
pub actual_selected_shard_count: usize,
pub exhaustive: bool,
pub eligible_shards: Vec<DeviceId>,
pub ranked_shards: Vec<RankedShard>,
pub selected_shards: Vec<DeviceId>,
pub skipped_shards: Vec<DeviceId>,
pub outcomes: Vec<ShardSearchOutcome>,
pub fallback_reason: Option<String>,
pub final_result_ids: Vec<String>,
pub merge_digest: String,
pub receipt_digest: String,
pub recorded_at: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct RoutedSearchResponse {
pub results: Vec<RoutedSearchResult>,
pub routing_receipt: ShardRoutingReceipt,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ShardCacheMetrics {
pub len: usize,
pub capacity: usize,
pub total_opens: u64,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ShardAggregateStats {
pub shards: u64,
pub facts: u64,
pub documents: u64,
pub chunks: u64,
pub messages: u64,
pub searches: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ShardIntegrityStatus {
pub device_id: DeviceId,
pub relative_path: PathBuf,
pub status: String,
pub detail: String,
}