use std::collections::{HashMap, HashSet};
use dejadb_cal::store_types::GrainTypeDiversityConfig;
use dejadb_core::types::GrainType;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Allocation {
Full,
Summary,
Omit,
}
pub struct ScoredEntry {
pub priority: f32,
pub full_tokens: usize,
pub summary_tokens: usize,
pub original_index: usize,
pub grain_type: GrainType,
}
pub fn allocate(entries: &mut [ScoredEntry], token_budget: Option<usize>) -> Vec<Allocation> {
let n = entries.len();
if n == 0 {
return Vec::new();
}
let Some(budget) = token_budget else {
return vec![Allocation::Full; n];
};
entries.sort_by(|a, b| {
b.priority
.partial_cmp(&a.priority)
.unwrap_or(std::cmp::Ordering::Equal)
});
let full_threshold = budget * 70 / 100;
let mut result_by_original = vec![Allocation::Omit; n];
let mut used = 0usize;
for entry in entries.iter() {
if used + entry.full_tokens <= full_threshold {
result_by_original[entry.original_index] = Allocation::Full;
used += entry.full_tokens;
}
}
result_by_original
}
pub fn allocate_with_diversity(
entries: &mut [ScoredEntry],
token_budget: Option<usize>,
diversity: &GrainTypeDiversityConfig,
) -> Vec<Allocation> {
let n = entries.len();
if n == 0 {
return Vec::new();
}
let Some(budget) = token_budget else {
return vec![Allocation::Full; n];
};
let mut groups: HashMap<GrainType, Vec<usize>> = HashMap::new();
for (i, entry) in entries.iter().enumerate() {
groups.entry(entry.grain_type).or_default().push(i);
}
for indices in groups.values_mut() {
indices.sort_by(|&a, &b| {
entries[b]
.priority
.partial_cmp(&entries[a].priority)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let mut reserved: HashSet<usize> = HashSet::new();
for indices in groups.values() {
let take = (diversity.min_per_type as usize).min(indices.len());
for &idx in &indices[..take] {
reserved.insert(idx);
}
}
let cap = (budget as f64 * diversity.max_reservation_pct as f64) as usize;
let reserved_tokens: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
if reserved_tokens > cap {
let mut removable: Vec<usize> = reserved.iter().copied().collect();
removable.sort_by(|&a, &b| {
entries[a]
.priority
.partial_cmp(&entries[b].priority)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut type_counts: HashMap<GrainType, usize> = HashMap::new();
for &idx in &reserved {
*type_counts.entry(entries[idx].grain_type).or_insert(0) += 1;
}
let mut current_tokens = reserved_tokens;
for &idx in &removable {
if current_tokens <= cap {
break;
}
let gt = entries[idx].grain_type;
let count = type_counts.get(>).copied().unwrap_or(0);
if count <= 1 {
continue; }
reserved.remove(&idx);
current_tokens -= entries[idx].full_tokens;
type_counts.insert(gt, count - 1);
}
}
let mut result = vec![Allocation::Omit; n];
let reserved_used: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
for &i in &reserved {
result[entries[i].original_index] = Allocation::Full;
}
let remaining_budget = budget.saturating_sub(reserved_used);
let threshold = remaining_budget * 70 / 100;
let mut non_reserved: Vec<usize> = (0..n).filter(|i| !reserved.contains(i)).collect();
non_reserved.sort_by(|&a, &b| {
entries[b]
.priority
.partial_cmp(&entries[a].priority)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut used = 0usize;
for idx in non_reserved {
if used + entries[idx].full_tokens <= threshold {
result[entries[idx].original_index] = Allocation::Full;
used += entries[idx].full_tokens;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(priority: f32, full_tokens: usize, summary_tokens: usize, idx: usize) -> ScoredEntry {
ScoredEntry {
priority,
full_tokens,
summary_tokens,
original_index: idx,
grain_type: GrainType::Fact,
}
}
fn typed_entry(
priority: f32,
full_tokens: usize,
grain_type: GrainType,
idx: usize,
) -> ScoredEntry {
ScoredEntry {
priority,
full_tokens,
summary_tokens: full_tokens / 3,
original_index: idx,
grain_type,
}
}
#[test]
fn test_no_budget_all_full() {
let mut entries = vec![entry(0.5, 100, 30, 0), entry(0.8, 200, 60, 1)];
let allocs = allocate(&mut entries, None);
assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
}
#[test]
fn test_empty_entries() {
let mut entries: Vec<ScoredEntry> = Vec::new();
let allocs = allocate(&mut entries, Some(1000));
assert!(allocs.is_empty());
}
#[test]
fn test_budget_overflow_omit() {
let mut entries = vec![entry(0.9, 800, 200, 0), entry(0.5, 800, 200, 1)];
let allocs = allocate(&mut entries, Some(1000));
assert_eq!(allocs[0], Allocation::Omit);
assert_eq!(allocs[1], Allocation::Omit);
}
#[test]
fn test_budget_full_then_omit() {
let mut entries = vec![
entry(0.9, 500, 150, 0),
entry(0.7, 500, 150, 1),
entry(0.3, 500, 150, 2),
];
let allocs = allocate(&mut entries, Some(1000));
assert_eq!(allocs[0], Allocation::Full);
assert_eq!(allocs[1], Allocation::Omit);
assert_eq!(allocs[2], Allocation::Omit);
}
#[test]
fn test_priority_ordering_respected() {
let mut entries = vec![entry(0.3, 100, 30, 0), entry(0.9, 100, 30, 1)];
let allocs = allocate(&mut entries, Some(200));
assert_eq!(allocs[0], Allocation::Omit);
assert_eq!(allocs[1], Allocation::Full);
}
#[test]
fn test_exceeds_full_threshold_omitted() {
let mut entries = vec![entry(0.9, 600, 100, 0), entry(0.5, 600, 100, 1)];
let allocs = allocate(&mut entries, Some(1000));
assert_eq!(allocs[0], Allocation::Full);
assert_eq!(allocs[1], Allocation::Omit);
}
#[test]
fn test_diversity_no_budget_all_full() {
let mut entries = vec![
typed_entry(0.9, 100, GrainType::Fact, 0),
typed_entry(0.5, 100, GrainType::Goal, 1),
];
let cfg = GrainTypeDiversityConfig::default();
let allocs = allocate_with_diversity(&mut entries, None, &cfg);
assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
}
#[test]
fn test_diversity_empty() {
let mut entries: Vec<ScoredEntry> = Vec::new();
let cfg = GrainTypeDiversityConfig::default();
let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
assert!(allocs.is_empty());
}
#[test]
fn test_diversity_reserves_rare_type() {
let mut entries = vec![
typed_entry(0.9, 200, GrainType::Fact, 0),
typed_entry(0.8, 200, GrainType::Fact, 1),
typed_entry(0.7, 200, GrainType::Fact, 2),
typed_entry(0.1, 200, GrainType::Goal, 3),
];
let cfg = GrainTypeDiversityConfig {
min_per_type: 1,
max_reservation_pct: 0.50,
};
let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
assert_eq!(allocs[3], Allocation::Full, "Goal should be reserved");
assert_eq!(allocs[0], Allocation::Full, "Top Fact should be reserved");
}
#[test]
fn test_diversity_trims_when_over_cap() {
let mut entries = vec![
typed_entry(0.9, 300, GrainType::Fact, 0),
typed_entry(0.5, 300, GrainType::Event, 1),
typed_entry(0.1, 300, GrainType::Goal, 2),
];
let cfg = GrainTypeDiversityConfig {
min_per_type: 1,
max_reservation_pct: 0.05, };
let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
assert_eq!(allocs[0], Allocation::Full);
assert_eq!(allocs[1], Allocation::Full);
assert_eq!(allocs[2], Allocation::Full);
}
}