1use std::collections::{HashMap, HashSet};
15
16use areev_cal::store_types::GrainTypeDiversityConfig;
17use areev_core::types::GrainType;
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum Allocation {
22 Full,
24 Summary,
26 Omit,
28}
29
30pub struct ScoredEntry {
32 pub priority: f32,
33 pub full_tokens: usize,
34 pub summary_tokens: usize,
35 pub original_index: usize,
37 pub grain_type: GrainType,
39}
40
41pub fn allocate(entries: &mut [ScoredEntry], token_budget: Option<usize>) -> Vec<Allocation> {
54 let n = entries.len();
55 if n == 0 {
56 return Vec::new();
57 }
58
59 let Some(budget) = token_budget else {
60 return vec![Allocation::Full; n];
62 };
63
64 entries.sort_by(|a, b| {
66 b.priority
67 .partial_cmp(&a.priority)
68 .unwrap_or(std::cmp::Ordering::Equal)
69 });
70
71 let full_threshold = budget * 70 / 100;
72 let summary_threshold = budget * 95 / 100;
73
74 let mut result_by_original = vec![Allocation::Omit; n];
76 let mut used = 0usize;
77
78 for entry in entries.iter() {
79 if used + entry.full_tokens <= full_threshold {
80 result_by_original[entry.original_index] = Allocation::Full;
81 used += entry.full_tokens;
82 } else if used + entry.summary_tokens <= summary_threshold {
83 result_by_original[entry.original_index] = Allocation::Summary;
84 used += entry.summary_tokens;
85 }
86 }
88
89 result_by_original
90}
91
92pub fn allocate_with_diversity(
109 entries: &mut [ScoredEntry],
110 token_budget: Option<usize>,
111 diversity: &GrainTypeDiversityConfig,
112) -> Vec<Allocation> {
113 let n = entries.len();
114 if n == 0 {
115 return Vec::new();
116 }
117
118 let Some(budget) = token_budget else {
119 return vec![Allocation::Full; n];
120 };
121
122 let mut groups: HashMap<GrainType, Vec<usize>> = HashMap::new();
124 for (i, entry) in entries.iter().enumerate() {
125 groups.entry(entry.grain_type).or_default().push(i);
126 }
127
128 for indices in groups.values_mut() {
130 indices.sort_by(|&a, &b| {
131 entries[b]
132 .priority
133 .partial_cmp(&entries[a].priority)
134 .unwrap_or(std::cmp::Ordering::Equal)
135 });
136 }
137
138 let mut reserved: HashSet<usize> = HashSet::new();
140 for indices in groups.values() {
141 let take = (diversity.min_per_type as usize).min(indices.len());
142 for &idx in &indices[..take] {
143 reserved.insert(idx);
144 }
145 }
146
147 let cap = (budget as f64 * diversity.max_reservation_pct as f64) as usize;
149 let reserved_tokens: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
150
151 if reserved_tokens > cap {
152 let mut removable: Vec<usize> = reserved.iter().copied().collect();
154 removable.sort_by(|&a, &b| {
155 entries[a]
156 .priority
157 .partial_cmp(&entries[b].priority)
158 .unwrap_or(std::cmp::Ordering::Equal)
159 });
160
161 let mut type_counts: HashMap<GrainType, usize> = HashMap::new();
163 for &idx in &reserved {
164 *type_counts.entry(entries[idx].grain_type).or_insert(0) += 1;
165 }
166
167 let mut current_tokens = reserved_tokens;
168 for &idx in &removable {
169 if current_tokens <= cap {
170 break;
171 }
172 let gt = entries[idx].grain_type;
173 let count = type_counts.get(>).copied().unwrap_or(0);
174 if count <= 1 {
175 continue; }
177 reserved.remove(&idx);
178 current_tokens -= entries[idx].full_tokens;
179 type_counts.insert(gt, count - 1);
180 }
181 }
182
183 let mut result = vec![Allocation::Omit; n];
185 let reserved_used: usize = reserved.iter().map(|&i| entries[i].full_tokens).sum();
186 for &i in &reserved {
187 result[entries[i].original_index] = Allocation::Full;
188 }
189
190 let remaining_budget = budget.saturating_sub(reserved_used);
193 let threshold = remaining_budget * 70 / 100;
194 let summary_threshold = remaining_budget * 95 / 100;
195
196 let mut non_reserved: Vec<usize> = (0..n).filter(|i| !reserved.contains(i)).collect();
198 non_reserved.sort_by(|&a, &b| {
199 entries[b]
200 .priority
201 .partial_cmp(&entries[a].priority)
202 .unwrap_or(std::cmp::Ordering::Equal)
203 });
204
205 let mut used = 0usize;
206 for idx in non_reserved {
207 if used + entries[idx].full_tokens <= threshold {
208 result[entries[idx].original_index] = Allocation::Full;
209 used += entries[idx].full_tokens;
210 } else if used + entries[idx].summary_tokens <= summary_threshold {
211 result[entries[idx].original_index] = Allocation::Summary;
212 used += entries[idx].summary_tokens;
213 }
214 }
215
216 result
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn entry(priority: f32, full_tokens: usize, summary_tokens: usize, idx: usize) -> ScoredEntry {
225 ScoredEntry {
226 priority,
227 full_tokens,
228 summary_tokens,
229 original_index: idx,
230 grain_type: GrainType::Fact,
231 }
232 }
233
234 fn typed_entry(
236 priority: f32,
237 full_tokens: usize,
238 grain_type: GrainType,
239 idx: usize,
240 ) -> ScoredEntry {
241 ScoredEntry {
242 priority,
243 full_tokens,
244 summary_tokens: full_tokens / 3,
245 original_index: idx,
246 grain_type,
247 }
248 }
249
250 #[test]
251 fn test_no_budget_all_full() {
252 let mut entries = vec![entry(0.5, 100, 30, 0), entry(0.8, 200, 60, 1)];
253 let allocs = allocate(&mut entries, None);
254 assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
255 }
256
257 #[test]
258 fn test_empty_entries() {
259 let mut entries: Vec<ScoredEntry> = Vec::new();
260 let allocs = allocate(&mut entries, Some(1000));
261 assert!(allocs.is_empty());
262 }
263
264 #[test]
265 fn test_budget_overflow_degrades_then_omits() {
266 let mut entries = vec![entry(0.9, 800, 200, 0), entry(0.5, 800, 200, 1)];
267 let allocs = allocate(&mut entries, Some(300));
271 assert_eq!(allocs[0], Allocation::Summary);
272 assert_eq!(allocs[1], Allocation::Omit);
273 }
274
275 #[test]
276 fn test_budget_full_then_summary() {
277 let mut entries = vec![
278 entry(0.9, 500, 150, 0),
279 entry(0.7, 500, 150, 1),
280 entry(0.3, 500, 150, 2),
281 ];
282 let allocs = allocate(&mut entries, Some(1000));
287 assert_eq!(allocs[0], Allocation::Full);
288 assert_eq!(allocs[1], Allocation::Summary);
289 assert_eq!(allocs[2], Allocation::Summary);
290 }
291
292 #[test]
293 fn test_priority_ordering_respected() {
294 let mut entries = vec![entry(0.3, 100, 30, 0), entry(0.9, 100, 30, 1)];
295 let allocs = allocate(&mut entries, Some(200));
298 assert_eq!(allocs[0], Allocation::Summary);
300 assert_eq!(allocs[1], Allocation::Full);
301 }
302
303 #[test]
304 fn test_exceeds_full_threshold_degrades_to_summary() {
305 let mut entries = vec![entry(0.9, 600, 100, 0), entry(0.5, 600, 100, 1)];
306 let allocs = allocate(&mut entries, Some(1000));
309 assert_eq!(allocs[0], Allocation::Full);
310 assert_eq!(allocs[1], Allocation::Summary);
311 }
312
313 #[test]
318 fn test_diversity_no_budget_all_full() {
319 let mut entries = vec![
320 typed_entry(0.9, 100, GrainType::Fact, 0),
321 typed_entry(0.5, 100, GrainType::Goal, 1),
322 ];
323 let cfg = GrainTypeDiversityConfig::default();
324 let allocs = allocate_with_diversity(&mut entries, None, &cfg);
325 assert_eq!(allocs, vec![Allocation::Full, Allocation::Full]);
326 }
327
328 #[test]
329 fn test_diversity_empty() {
330 let mut entries: Vec<ScoredEntry> = Vec::new();
331 let cfg = GrainTypeDiversityConfig::default();
332 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
333 assert!(allocs.is_empty());
334 }
335
336 #[test]
337 fn test_diversity_reserves_rare_type() {
338 let mut entries = vec![
342 typed_entry(0.9, 200, GrainType::Fact, 0),
343 typed_entry(0.8, 200, GrainType::Fact, 1),
344 typed_entry(0.7, 200, GrainType::Fact, 2),
345 typed_entry(0.1, 200, GrainType::Goal, 3),
346 ];
347 let cfg = GrainTypeDiversityConfig {
348 min_per_type: 1,
349 max_reservation_pct: 0.50,
350 };
351 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
357 assert_eq!(allocs[3], Allocation::Full, "Goal should be reserved");
358 assert_eq!(allocs[0], Allocation::Full, "Top Fact should be reserved");
359 }
360
361 #[test]
362 fn test_diversity_trims_when_over_cap() {
363 let mut entries = vec![
367 typed_entry(0.9, 300, GrainType::Fact, 0),
368 typed_entry(0.5, 300, GrainType::Event, 1),
369 typed_entry(0.1, 300, GrainType::Goal, 2),
370 ];
371 let cfg = GrainTypeDiversityConfig {
372 min_per_type: 1,
373 max_reservation_pct: 0.05, };
375 let allocs = allocate_with_diversity(&mut entries, Some(1000), &cfg);
376 assert_eq!(allocs[0], Allocation::Full);
381 assert_eq!(allocs[1], Allocation::Full);
382 assert_eq!(allocs[2], Allocation::Full);
383 }
384}