use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::path::Path;
use super::context_reduce::reduce_source;
use super::map::build_map;
use super::skeleton::extract_rust_skeleton;
use super::{ContextMode, Graph, NodeLayer};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestedMode {
Auto,
Fixed(ContextMode),
}
impl RequestedMode {
pub fn parse(raw: &str) -> Result<Self, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"map" => Ok(Self::Fixed(ContextMode::Map)),
"lite" => Ok(Self::Fixed(ContextMode::Lite)),
"compact" | "skeleton" => Ok(Self::Fixed(ContextMode::Compact)),
"full" => Ok(Self::Fixed(ContextMode::Full)),
"full_extended" => Ok(Self::Fixed(ContextMode::FullExtended)),
other => Err(format!(
"unknown context mode '{other}' \
(expected auto|map|lite|compact|full|full_extended)"
)),
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Fixed(mode) => mode.name(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FitBudget {
pub context_length: usize,
pub output_reserve: usize,
pub fit_ratio: f64,
}
impl FitBudget {
pub fn new(context_length: usize, max_tokens: usize, fit_ratio: f64) -> Self {
Self {
context_length,
output_reserve: max_tokens.min(context_length / 4),
fit_ratio,
}
}
pub fn usable(&self) -> usize {
(self.context_length.saturating_sub(self.output_reserve) as f64 * self.fit_ratio) as usize
}
}
pub const TIER_LADDER: [ContextMode; 5] = [
ContextMode::FullExtended,
ContextMode::Full,
ContextMode::Compact,
ContextMode::Lite,
ContextMode::Map,
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FitOutcome {
pub mode: ContextMode,
pub measured_tokens: usize,
pub budget_tokens: usize,
pub fits: bool,
}
const SIGNATURE_FRACTION: f64 = 0.18;
const COMMENT_STRIPPED_FRACTION: f64 = 0.82;
pub struct TierMeasurer<'a> {
graph: &'a Graph,
root: &'a Path,
cache: RefCell<HashMap<&'static str, usize>>,
io_reads: Cell<usize>,
}
impl<'a> TierMeasurer<'a> {
pub fn new(graph: &'a Graph, root: &'a Path) -> Self {
Self {
graph,
root,
cache: RefCell::new(HashMap::new()),
io_reads: Cell::new(0),
}
}
pub fn io_reads(&self) -> usize {
self.io_reads.get()
}
pub fn measure(&self, mode: &ContextMode) -> usize {
match mode {
ContextMode::FullExtended => self.measured_full_extended(),
ContextMode::Full => self.measured_full(),
ContextMode::Compact => self.cached("compact", Self::measure_compact),
ContextMode::Lite => self.cached("lite", Self::measure_lite),
ContextMode::Map => self.cached("map", Self::measure_map),
ContextMode::Preset(_) | ContextMode::Custom => self.measured_full(),
}
}
fn cached(&self, key: &'static str, f: fn(&Self) -> usize) -> usize {
if let Some(hit) = self.cache.borrow().get(key) {
return *hit;
}
let value = f(self);
self.cache.borrow_mut().insert(key, value);
value
}
fn measured_full_extended(&self) -> usize {
self.graph
.nodes
.iter()
.filter(|n| matches!(n.layer, NodeLayer::Code | NodeLayer::Test))
.map(|n| n.tokens)
.sum()
}
fn measured_full(&self) -> usize {
self.graph
.nodes
.iter()
.filter(|n| n.layer == NodeLayer::Code)
.map(|n| n.tokens.saturating_sub(n.inline_test_tokens))
.sum()
}
fn read_node_source(&self, node: &super::Node) -> Option<(String, String)> {
let rel = node.path.as_deref()?;
let src = std::fs::read_to_string(self.root.join(rel)).ok()?;
self.io_reads.set(self.io_reads.get() + 1);
Some((rel.to_string(), src))
}
fn measure_compact(&self) -> usize {
let mut total = 0usize;
for node in self
.graph
.nodes
.iter()
.filter(|n| n.layer == NodeLayer::Code)
{
let code_tokens = node.tokens.saturating_sub(node.inline_test_tokens);
total += match self.read_node_source(node) {
Some((_, src)) => crate::token_count::estimate_content_tokens(&reduce_source(&src)),
None => (code_tokens as f64 * COMMENT_STRIPPED_FRACTION).round() as usize,
};
}
total
}
fn measure_lite(&self) -> usize {
let mut total = 0usize;
for node in self
.graph
.nodes
.iter()
.filter(|n| n.layer == NodeLayer::Code)
{
let code_tokens = node.tokens.saturating_sub(node.inline_test_tokens);
total += match self.read_node_source(node) {
Some((rel, src)) if rel.ends_with(".rs") => {
extract_rust_skeleton(Path::new(&rel), &src).token_count
}
_ => (code_tokens as f64 * SIGNATURE_FRACTION).round() as usize,
};
}
total
}
fn measure_map(&self) -> usize {
build_map(self.graph, self.root).map_tokens
}
}
pub fn fit_tier(measurer: &TierMeasurer, budget: &FitBudget) -> FitOutcome {
let usable = budget.usable();
for mode in TIER_LADDER {
let measured = measurer.measure(&mode);
if measured <= usable {
return FitOutcome {
mode,
measured_tokens: measured,
budget_tokens: usable,
fits: true,
};
}
}
FitOutcome {
mode: ContextMode::Map,
measured_tokens: measurer.measure(&ContextMode::Map),
budget_tokens: usable,
fits: false,
}
}
#[cfg(test)]
#[path = "../../tests/unit/evolve/context_fit_test.rs"]
mod context_fit_test;