use super::{EpisodeMetadata, RoutingResult, ScopeFilter, ShardConfig};
pub struct ShardRouter {
config: ShardConfig,
}
impl ShardRouter {
pub fn new(config: ShardConfig) -> Self {
Self { config }
}
#[must_use]
pub fn default_config() -> Self {
Self::new(ShardConfig::default())
}
#[must_use]
pub fn route(&self, filter: &ScopeFilter, episodes: &[EpisodeMetadata]) -> RoutingResult {
if episodes.is_empty() {
return RoutingResult::empty(filter.clone());
}
let original_count = episodes.len();
let mut scored_candidates: Vec<(uuid::Uuid, f32)> = Vec::new();
for ep in episodes {
let score = self.compute_route_score(filter, ep);
if !self.passes_hard_filters(filter, ep) {
continue;
}
if score > 0.0 {
scored_candidates.push((ep.episode_id, score));
}
}
scored_candidates
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let capped = scored_candidates.len() > self.config.max_candidates;
scored_candidates.truncate(self.config.max_candidates);
let candidates: Vec<uuid::Uuid> = scored_candidates.iter().map(|(id, _)| *id).collect();
let scores: Vec<f32> = scored_candidates.iter().map(|(_, s)| *s).collect();
let filtered_count = candidates.len();
RoutingResult {
candidates,
original_count,
filtered_count,
capped,
filter: filter.clone(),
scores,
}
}
fn compute_route_score(&self, filter: &ScopeFilter, ep: &EpisodeMetadata) -> f32 {
let tag_score = self.compute_tag_score(filter, ep);
let task_type_score = self.compute_task_type_score(filter, ep);
let timeframe_score = self.compute_timeframe_score(filter, ep);
let base_score = tag_score * self.config.tag_weight
+ task_type_score * self.config.task_type_weight
+ timeframe_score * self.config.timeframe_weight;
let final_score = if self.config.use_temporal_decay {
let decay = self.compute_temporal_decay(ep);
base_score * decay
} else {
base_score
};
let success_bonus = if let Some(min_rate) = filter.min_success_rate {
if ep.success_rate >= min_rate {
0.1 } else {
0.0
}
} else {
0.0
};
final_score + success_bonus
}
fn compute_tag_score(&self, filter: &ScopeFilter, ep: &EpisodeMetadata) -> f32 {
if filter.required_tags.is_empty() {
return 0.5; }
let matching_tags = filter.required_tags.intersection(&ep.tags).count();
if matching_tags == 0 {
return 0.0;
}
matching_tags as f32 / filter.required_tags.len().max(1) as f32
}
fn compute_task_type_score(&self, filter: &ScopeFilter, ep: &EpisodeMetadata) -> f32 {
if filter.required_task_types.is_empty() {
return 0.5; }
if filter
.required_task_types
.iter()
.any(|tt| ep.matches_task_type(tt))
{
return 1.0;
}
0.0
}
fn compute_timeframe_score(&self, filter: &ScopeFilter, ep: &EpisodeMetadata) -> f32 {
match &filter.time_range {
Some(range) => {
if range.contains(ep.created_at) {
let age_days = ep.age_days();
let range_days = range.duration_days();
if range_days > 0 {
1.0 - (age_days as f32 / range_days as f32).min(1.0)
} else {
1.0
}
} else {
0.0
}
}
None => 0.5, }
}
fn compute_temporal_decay(&self, ep: &EpisodeMetadata) -> f32 {
let age_days = ep.age_days();
if age_days <= 7 {
1.0 } else if age_days <= self.config.stale_days {
1.0 - (0.5 * (age_days - 7) as f32 / (self.config.stale_days - 7) as f32)
} else {
0.3 }
}
fn passes_hard_filters(&self, filter: &ScopeFilter, ep: &EpisodeMetadata) -> bool {
if !filter.required_tags.is_empty() {
let has_match = filter.required_tags.intersection(&ep.tags).count() > 0;
if !has_match {
return false;
}
}
if filter.excluded_tags.iter().any(|t| ep.has_tag(t)) {
return false;
}
if !filter.required_task_types.is_empty()
&& !filter
.required_task_types
.iter()
.any(|tt| ep.matches_task_type(tt))
{
return false;
}
if let Some(range) = &filter.time_range {
if !range.contains(ep.created_at) {
return false;
}
}
if let Some(min_rate) = filter.min_success_rate {
if ep.success_rate < min_rate {
return false;
}
}
true
}
#[must_use]
pub fn estimate_reduction(&self, filter: &ScopeFilter, _total_episodes: usize) -> f32 {
if !filter.has_constraints() {
return 0.0;
}
let constraint_factor = filter.constraint_count() as f32 * 0.15;
let time_factor = if filter.time_range.is_some() {
0.2
} else {
0.0
};
((constraint_factor + time_factor) * 100.0).min(80.0)
}
#[must_use]
pub fn config(&self) -> &ShardConfig {
&self.config
}
}