use std::collections::{HashMap, HashSet, VecDeque};
use super::code_graph::CodeGraph;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ContextTier {
Describe,
Work,
Integrate,
Edit,
}
impl ContextTier {
pub fn default_tokens(&self) -> usize {
match self {
ContextTier::Describe => 50,
ContextTier::Work => 200,
ContextTier::Integrate => 400,
ContextTier::Edit => 800,
}
}
pub fn from_hops(hops: usize) -> Self {
match hops {
0 => ContextTier::Edit,
1 => ContextTier::Integrate,
2 => ContextTier::Work,
_ => ContextTier::Describe,
}
}
pub fn as_str(&self) -> &'static str {
match self {
ContextTier::Describe => "L1-Describe",
ContextTier::Work => "L2-Work",
ContextTier::Integrate => "L3-Integrate",
ContextTier::Edit => "L4-Edit",
}
}
}
impl std::fmt::Display for ContextTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct TierAssignment {
pub node_id: String,
pub file_path: Option<String>,
pub name: String,
pub tier: ContextTier,
pub hops: usize,
pub estimated_tokens: usize,
}
#[derive(Debug, Clone)]
pub struct TierAllocation {
pub focus_node: String,
pub assignments: Vec<TierAssignment>,
pub total_tokens: usize,
pub budget: usize,
pub excluded_count: usize,
pub downgraded_count: usize,
}
impl TierAllocation {
pub fn at_tier(&self, tier: ContextTier) -> Vec<&TierAssignment> {
self.assignments.iter().filter(|a| a.tier == tier).collect()
}
pub fn for_node(&self, node_id: &str) -> Option<&TierAssignment> {
self.assignments.iter().find(|a| a.node_id == node_id)
}
pub fn utilization_pct(&self) -> f64 {
if self.budget == 0 {
return 0.0;
}
(self.total_tokens as f64 / self.budget as f64 * 100.0).min(100.0)
}
pub fn summary(&self) -> String {
let l4 = self.at_tier(ContextTier::Edit).len();
let l3 = self.at_tier(ContextTier::Integrate).len();
let l2 = self.at_tier(ContextTier::Work).len();
let l1 = self.at_tier(ContextTier::Describe).len();
format!(
"Focus: {} | L4:{} L3:{} L2:{} L1:{} | {}/{} tokens ({:.0}%) | excluded:{} downgraded:{}",
self.focus_node,
l4, l3, l2, l1,
self.total_tokens, self.budget,
self.utilization_pct(),
self.excluded_count,
self.downgraded_count,
)
}
}
#[derive(Debug, Clone)]
pub struct TierAllocatorConfig {
pub context_window: usize,
pub system_reserve: usize,
pub output_reserve: usize,
pub tier_tokens: [usize; 4], }
impl TierAllocatorConfig {
pub fn for_context_window(context_window: usize) -> Self {
let system_reserve = if context_window <= 32768 { 2000 } else { 4000 };
let output_pct = if context_window <= 32768 { 0.40 } else { 0.30 };
let output_reserve = (context_window as f64 * output_pct) as usize;
Self {
context_window,
system_reserve,
output_reserve,
tier_tokens: [
ContextTier::Describe.default_tokens(),
ContextTier::Work.default_tokens(),
ContextTier::Integrate.default_tokens(),
ContextTier::Edit.default_tokens(),
],
}
}
pub fn content_budget(&self) -> usize {
self.context_window
.saturating_sub(self.system_reserve)
.saturating_sub(self.output_reserve)
}
pub fn tokens_for_tier(&self, tier: ContextTier) -> usize {
match tier {
ContextTier::Describe => self.tier_tokens[0],
ContextTier::Work => self.tier_tokens[1],
ContextTier::Integrate => self.tier_tokens[2],
ContextTier::Edit => self.tier_tokens[3],
}
}
}
impl Default for TierAllocatorConfig {
fn default() -> Self {
Self::for_context_window(131072) }
}
fn bfs_hop_distances(graph: &CodeGraph, source_id: &str) -> HashMap<String, usize> {
let mut distances: HashMap<String, usize> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
distances.insert(source_id.to_string(), 0);
queue.push_back(source_id.to_string());
while let Some(current) = queue.pop_front() {
let current_dist = distances[¤t];
for dep in graph.dependencies(¤t) {
if !distances.contains_key(&dep.id) {
distances.insert(dep.id.clone(), current_dist + 1);
queue.push_back(dep.id.clone());
}
}
for dep in graph.dependents(¤t) {
if !distances.contains_key(&dep.id) {
distances.insert(dep.id.clone(), current_dist + 1);
queue.push_back(dep.id.clone());
}
}
}
distances
}
pub fn allocate_tiers(
graph: &CodeGraph,
focus_node_id: &str,
config: &TierAllocatorConfig,
) -> TierAllocation {
let budget = config.content_budget();
let distances = bfs_hop_distances(graph, focus_node_id);
if distances.is_empty() {
return TierAllocation {
focus_node: focus_node_id.to_string(),
assignments: Vec::new(),
total_tokens: 0,
budget,
excluded_count: 0,
downgraded_count: 0,
};
}
let mut assignments: Vec<TierAssignment> = distances
.iter()
.filter_map(|(node_id, &hops)| {
let node = graph.get_node_by_id(node_id)?;
let tier = ContextTier::from_hops(hops);
Some(TierAssignment {
node_id: node_id.clone(),
file_path: node.file_path.clone(),
name: node.name.clone(),
tier,
hops,
estimated_tokens: config.tokens_for_tier(tier),
})
})
.collect();
assignments.sort_by_key(|a| (a.hops, a.name.clone()));
let mut total: usize = assignments.iter().map(|a| a.estimated_tokens).sum();
let mut downgraded_count = 0;
let mut excluded_count = 0;
if total > budget {
let mut indices_by_distance: Vec<usize> = (0..assignments.len()).collect();
indices_by_distance.sort_by(|&a, &b| assignments[b].hops.cmp(&assignments[a].hops));
for &idx in &indices_by_distance {
if total <= budget {
break;
}
if assignments[idx].hops == 0 {
continue;
}
let current_tier = assignments[idx].tier;
let downgraded_tier = match current_tier {
ContextTier::Edit => Some(ContextTier::Integrate),
ContextTier::Integrate => Some(ContextTier::Work),
ContextTier::Work => Some(ContextTier::Describe),
ContextTier::Describe => None, };
if let Some(new_tier) = downgraded_tier {
let old_tokens = assignments[idx].estimated_tokens;
let new_tokens = config.tokens_for_tier(new_tier);
assignments[idx].tier = new_tier;
assignments[idx].estimated_tokens = new_tokens;
total = total.saturating_sub(old_tokens) + new_tokens;
downgraded_count += 1;
}
}
}
if total > budget {
let mut indices_by_distance: Vec<usize> = (0..assignments.len()).collect();
indices_by_distance.sort_by(|&a, &b| assignments[b].hops.cmp(&assignments[a].hops));
let mut to_exclude: HashSet<usize> = HashSet::new();
for &idx in &indices_by_distance {
if total <= budget {
break;
}
if assignments[idx].hops == 0 {
continue; }
total = total.saturating_sub(assignments[idx].estimated_tokens);
to_exclude.insert(idx);
excluded_count += 1;
}
let mut excluded_sorted: Vec<usize> = to_exclude.into_iter().collect();
excluded_sorted.sort_unstable_by(|a, b| b.cmp(a));
for idx in excluded_sorted {
assignments.remove(idx);
}
}
assignments.sort_by(|a, b| {
b.tier
.cmp(&a.tier)
.then_with(|| a.hops.cmp(&b.hops))
.then_with(|| a.name.cmp(&b.name))
});
TierAllocation {
focus_node: focus_node_id.to_string(),
assignments,
total_tokens: total,
budget,
excluded_count,
downgraded_count,
}
}
#[cfg(test)]
#[path = "../../tests/unit/analysis/tier_allocator/tier_allocator_test.rs"]
mod tests;