mod router;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use uuid::Uuid;
pub use router::ShardRouter;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ShardConfig {
pub max_candidates: usize,
pub min_candidates: usize,
pub tag_weight: f32,
pub task_type_weight: f32,
pub timeframe_weight: f32,
pub use_temporal_decay: bool,
pub stale_days: u32,
}
impl Default for ShardConfig {
fn default() -> Self {
Self {
max_candidates: 100,
min_candidates: 10,
tag_weight: 0.4,
task_type_weight: 0.3,
timeframe_weight: 0.3,
use_temporal_decay: true,
stale_days: 30,
}
}
}
impl ShardConfig {
#[must_use]
pub fn large_dataset() -> Self {
Self {
max_candidates: 500,
min_candidates: 50,
tag_weight: 0.5,
task_type_weight: 0.3,
timeframe_weight: 0.2,
use_temporal_decay: true,
stale_days: 60,
}
}
#[must_use]
pub fn small_dataset() -> Self {
Self {
max_candidates: 50,
min_candidates: 5,
tag_weight: 0.3,
task_type_weight: 0.3,
timeframe_weight: 0.4,
use_temporal_decay: true,
stale_days: 14,
}
}
#[must_use]
pub fn recent_focused() -> Self {
Self {
max_candidates: 100,
min_candidates: 10,
tag_weight: 0.2,
task_type_weight: 0.2,
timeframe_weight: 0.6,
use_temporal_decay: true,
stale_days: 7,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeFilter {
pub required_tags: HashSet<String>,
pub excluded_tags: HashSet<String>,
pub required_task_types: HashSet<String>,
pub time_range: Option<TimeRange>,
pub min_success_rate: Option<f32>,
}
impl ScopeFilter {
#[must_use]
pub fn new() -> Self {
Self {
required_tags: HashSet::new(),
excluded_tags: HashSet::new(),
required_task_types: HashSet::new(),
time_range: None,
min_success_rate: None,
}
}
#[must_use]
pub fn from_query_text(query: &str) -> Self {
let mut filter = Self::new();
let common_tags = [
"bug",
"fix",
"feature",
"refactor",
"test",
"security",
"performance",
"documentation",
"api",
"cli",
"mcp",
"storage",
"core",
];
for tag in common_tags {
if query.to_lowercase().contains(tag) {
filter.required_tags.insert(tag.to_string());
}
}
let task_types = ["implementation", "debugging", "testing", "review"];
for tt in task_types {
if query.to_lowercase().contains(tt) {
filter.required_task_types.insert(tt.to_string());
}
}
let lower = query.to_lowercase();
if lower.contains("recent") || lower.contains("latest") {
filter.time_range = Some(TimeRange::recent_days(7));
} else if lower.contains("today") {
filter.time_range = Some(TimeRange::today());
} else if lower.contains("this week") {
filter.time_range = Some(TimeRange::recent_days(7));
} else if lower.contains("this month") {
filter.time_range = Some(TimeRange::recent_days(30));
}
filter
}
pub fn require_tag(&mut self, tag: &str) {
self.required_tags.insert(tag.to_lowercase());
}
pub fn exclude_tag(&mut self, tag: &str) {
self.excluded_tags.insert(tag.to_lowercase());
}
pub fn require_task_type(&mut self, task_type: &str) {
self.required_task_types.insert(task_type.to_lowercase());
}
pub fn set_time_range(&mut self, range: TimeRange) {
self.time_range = Some(range);
}
pub fn set_min_success_rate(&mut self, rate: f32) {
self.min_success_rate = Some(rate);
}
#[must_use]
pub fn has_constraints(&self) -> bool {
!self.required_tags.is_empty()
|| !self.excluded_tags.is_empty()
|| !self.required_task_types.is_empty()
|| self.time_range.is_some()
|| self.min_success_rate.is_some()
}
#[must_use]
pub fn constraint_count(&self) -> usize {
self.required_tags.len()
+ self.excluded_tags.len()
+ self.required_task_types.len()
+ self.time_range.map_or(0, |_| 1)
+ self.min_success_rate.map_or(0, |_| 1)
}
}
impl Default for ScopeFilter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TimeRange {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
impl TimeRange {
#[must_use]
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
Self { start, end }
}
#[must_use]
pub fn recent_days(days: u32) -> Self {
let end = Utc::now();
let start = end - chrono::Duration::days(i64::from(days));
Self { start, end }
}
#[must_use]
pub fn today() -> Self {
let now = Utc::now();
let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
Self {
start: DateTime::from_naive_utc_and_offset(start, Utc),
end: now,
}
}
#[must_use]
pub fn this_week() -> Self {
Self::recent_days(7)
}
#[must_use]
pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
timestamp >= self.start && timestamp <= self.end
}
#[must_use]
pub fn duration_days(&self) -> u32 {
((self.end - self.start).num_days()).max(0) as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpisodeMetadata {
pub episode_id: Uuid,
pub tags: HashSet<String>,
pub task_type: String,
pub created_at: DateTime<Utc>,
pub success_rate: f32,
pub is_complete: bool,
}
impl EpisodeMetadata {
#[must_use]
pub fn new(
episode_id: Uuid,
tags: HashSet<String>,
task_type: String,
created_at: DateTime<Utc>,
) -> Self {
Self {
episode_id,
tags,
task_type,
created_at,
success_rate: 0.0,
is_complete: false,
}
}
pub fn set_success_rate(&mut self, rate: f32) {
self.success_rate = rate;
}
pub fn mark_complete(&mut self) {
self.is_complete = true;
}
#[must_use]
pub fn has_tag(&self, tag: &str) -> bool {
self.tags.contains(&tag.to_lowercase())
}
#[must_use]
pub fn matches_task_type(&self, task_type: &str) -> bool {
self.task_type.to_lowercase() == task_type.to_lowercase()
}
#[must_use]
pub fn age_days(&self) -> u32 {
((Utc::now() - self.created_at).num_days()).max(0) as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingResult {
pub candidates: Vec<Uuid>,
pub original_count: usize,
pub filtered_count: usize,
pub capped: bool,
pub filter: ScopeFilter,
pub scores: Vec<f32>,
}
impl RoutingResult {
#[must_use]
pub fn empty(filter: ScopeFilter) -> Self {
Self {
candidates: Vec::new(),
original_count: 0,
filtered_count: 0,
capped: false,
filter,
scores: Vec::new(),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.candidates.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.candidates.is_empty()
}
#[must_use]
pub fn reduction_ratio(&self) -> f32 {
if self.original_count == 0 {
return 0.0;
}
1.0 - (self.filtered_count as f32 / self.original_count as f32)
}
}
#[cfg(test)]
mod tests;