1use std::collections::{HashMap, HashSet};
12
13use dejadb_cal::store_types::GrainTypeDiversityConfig;
14use dejadb_core::types::GrainType;
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum Allocation {
19 Full,
21 Summary,
23 Omit,
25}
26
27pub struct ScoredEntry {
29 pub priority: f32,
30 pub full_tokens: usize,
31 pub summary_tokens: usize,
32 pub original_index: usize,
34 pub grain_type: GrainType,
36}
37
38pub fn allocate(entries: &mut [ScoredEntry], token_budget: Option<usize>) -> Vec<Allocation> {
50 let n = entries.len();
51 if n == 0 {
52 return Vec::new();
53 }
54
55 let Some(budget) = token_budget else {
56 return vec![Allocation::Full; n];
58 };
59
60 entries.sort_by(|a, b| {
62 b.priority
63 .partial_cmp(&a.priority)
64 .unwrap_or(std::cmp::Ordering::Equal)
65 });
66
67 let full_threshold = budget * 70 / 100;
68
69 let mut result_by_original = vec![Allocation::Omit; n];
71 let mut used = 0usize;
72
73 for entry in entries.iter() {
74 if used + entry.full_tokens <= full_threshold {
75 result_by_original[entry.original_index] = Allocation::Full;
76 used += entry.full_tokens;
77 }
78 }
80
81 result_by_original
82}
83
84pub fn allocate_with_diversity(
101 entries: &mut [ScoredEntry],
102 token_budget: Option<usize>,
103 diversity: &GrainTypeDiversityConfig,
104) -> Vec<Allocation> {
105 let n = entries.len();
106 if n == 0 {
107 return Vec::new();
108 }
109
110 let Some(budget) = token_budget else {
111 return vec![Allocation::Full; n];
112 };
113
114 let mut groups: HashMap<GrainType, Vec<usize>> = HashMap::new();
116 for (i, entry) in entries.iter().enumerate() {
117 groups.entry(entry.grain_type).or_default().push(i);
118 }
119
120 for indices in groups.values_mut() {
122 indices.sort_by(|&a, &b| {
123 entries[b]
124 .priority
125 .partial_cmp(&entries[a].priority)
126 .unwrap_or(std::cmp::Ordering::Equal)
127 });
128 }
129
130 let mut reserved: HashSet<usize> = HashSet::new();
132 for indices in groups.values() {
133 let take = (diversity.min_per_type as usize).min(indices.len());
134 for &idx in &indices[..take] {
135 reserved.insert(idx);
136 }
137 }
138
139 let cap = (budget as f64 * diversity.max_reservation_pct as f64) as usize;
141 let reserved_tokens: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
142
143 if reserved_tokens > cap {
144 let mut removable: Vec<usize> = reserved.iter().copied().collect();
146 removable.sort_by(|&a, &b| {
147 entries[a]
148 .priority
149 .partial_cmp(&entries[b].priority)
150 .unwrap_or(std::cmp::Ordering::Equal)
151 });
152
153 let mut type_counts: HashMap<GrainType, usize> = HashMap::new();
155 for &idx in &reserved {
156 *type_counts.entry(entries[idx].grain_type).or_insert(0) += 1;
157 }
158
159 let mut current_tokens = reserved_tokens;
160 for &idx in &removable {
161 if current_tokens <= cap {
162 break;
163 }
164 let gt = entries[idx].grain_type;
165 let count = type_counts.get(>).copied().unwrap_or(0);
166 if count <= 1 {
167 continue; }
169 reserved.remove(&idx);
170 current_tokens -= entries[idx].full_tokens;
171 type_counts.insert(gt, count - 1);
172 }
173 }
174
175 let mut result = vec![Allocation::Omit; n];
177 let reserved_used: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
178 for &i in &reserved {
179 result[entries[i].original_index] = Allocation::Full;
180 }
181
182 let remaining_budget = budget.saturating_sub(reserved_used);
184 let threshold = remaining_budget * 70 / 100;
185
186 let mut non_reserved: Vec<usize> = (0..n).filter(|i| !reserved.contains(i)).collect();
188 non_reserved.sort_by(|&a, &b| {
189 entries[b]
190 .priority
191 .partial_cmp(&entries[a].priority)
192 .unwrap_or(std::cmp::Ordering::Equal)
193 });
194
195 let mut used = 0usize;
196 for idx in non_reserved {
197 if used + entries[idx].full_tokens <= threshold {
198 result[entries[idx].original_index] = Allocation::Full;
199 used += entries[idx].full_tokens;
200 }
201 }
202
203 result
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn entry(priority: f32, full_tokens: usize, summary_tokens: usize, idx: usize) -> ScoredEntry {
212 ScoredEntry {
213 priority,
214 full_tokens,
215 summary_tokens,
216 original_index: idx,
217 grain_type: GrainType::Fact,
218 }
219 }
220
221 fn typed_entry(
223 priority: f32,
224 full_tokens: usize,
225 grain_type: GrainType,
226 idx: usize,
227 ) -> ScoredEntry {
228 ScoredEntry {
229 priority,
230 full_tokens,
231 summary_tokens: full_tokens / 3,
232 original_index: idx,
233 grain_type,
234 }
235 }
236
237 #[test]
238 fn test_no_budget_all_full() {
239 let mut entries = vec![entry(0.5, 100, 30, 0), entry(0.8, 200, 60, 1)];
240 let allocs = allocate(&mut entries, None);
241 assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
242 }
243
244 #[test]
245 fn test_empty_entries() {
246 let mut entries: Vec<ScoredEntry> = Vec::new();
247 let allocs = allocate(&mut entries, Some(1000));
248 assert!(allocs.is_empty());
249 }
250
251 #[test]
252 fn test_budget_overflow_omit() {
253 let mut entries = vec![entry(0.9, 800, 200, 0), entry(0.5, 800, 200, 1)];
254 let allocs = allocate(&mut entries, Some(1000));
256 assert_eq!(allocs[0], Allocation::Omit);
258 assert_eq!(allocs[1], Allocation::Omit);
259 }
260
261 #[test]
262 fn test_budget_full_then_omit() {
263 let mut entries = vec![
264 entry(0.9, 500, 150, 0),
265 entry(0.7, 500, 150, 1),
266 entry(0.3, 500, 150, 2),
267 ];
268 let allocs = allocate(&mut entries, Some(1000));
273 assert_eq!(allocs[0], Allocation::Full);
274 assert_eq!(allocs[1], Allocation::Omit);
275 assert_eq!(allocs[2], Allocation::Omit);
276 }
277
278 #[test]
279 fn test_priority_ordering_respected() {
280 let mut entries = vec![entry(0.3, 100, 30, 0), entry(0.9, 100, 30, 1)];
281 let allocs = allocate(&mut entries, Some(200));
284 assert_eq!(allocs[0], Allocation::Omit);
286 assert_eq!(allocs[1], Allocation::Full);
287 }
288
289 #[test]
290 fn test_exceeds_full_threshold_omitted() {
291 let mut entries = vec![entry(0.9, 600, 100, 0), entry(0.5, 600, 100, 1)];
292 let allocs = allocate(&mut entries, Some(1000));
295 assert_eq!(allocs[0], Allocation::Full);
296 assert_eq!(allocs[1], Allocation::Omit);
297 }
298
299 #[test]
304 fn test_diversity_no_budget_all_full() {
305 let mut entries = vec![
306 typed_entry(0.9, 100, GrainType::Fact, 0),
307 typed_entry(0.5, 100, GrainType::Goal, 1),
308 ];
309 let cfg = GrainTypeDiversityConfig::default();
310 let allocs = allocate_with_diversity(&mut entries, None, &cfg);
311 assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
312 }
313
314 #[test]
315 fn test_diversity_empty() {
316 let mut entries: Vec<ScoredEntry> = Vec::new();
317 let cfg = GrainTypeDiversityConfig::default();
318 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
319 assert!(allocs.is_empty());
320 }
321
322 #[test]
323 fn test_diversity_reserves_rare_type() {
324 let mut entries = vec![
328 typed_entry(0.9, 200, GrainType::Fact, 0),
329 typed_entry(0.8, 200, GrainType::Fact, 1),
330 typed_entry(0.7, 200, GrainType::Fact, 2),
331 typed_entry(0.1, 200, GrainType::Goal, 3),
332 ];
333 let cfg = GrainTypeDiversityConfig {
334 min_per_type: 1,
335 max_reservation_pct: 0.50,
336 };
337 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
343 assert_eq!(allocs[3], Allocation::Full, "Goal should be reserved");
344 assert_eq!(allocs[0], Allocation::Full, "Top Fact should be reserved");
345 }
346
347 #[test]
348 fn test_diversity_trims_when_over_cap() {
349 let mut entries = vec![
353 typed_entry(0.9, 300, GrainType::Fact, 0),
354 typed_entry(0.5, 300, GrainType::Event, 1),
355 typed_entry(0.1, 300, GrainType::Goal, 2),
356 ];
357 let cfg = GrainTypeDiversityConfig {
358 min_per_type: 1,
359 max_reservation_pct: 0.05, };
361 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
362 assert_eq!(allocs[0], Allocation::Full);
367 assert_eq!(allocs[1], Allocation::Full);
368 assert_eq!(allocs[2], Allocation::Full);
369 }
370}