use std::path::Path;
use crate::core::cache::SessionCache;
use crate::core::signatures;
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;
struct FileCandidate {
path: String,
score: f64,
tokens_full: usize,
tokens_map: usize,
tokens_sig: usize,
}
pub fn handle(
cache: &mut SessionCache,
paths: &[String],
budget: usize,
crp_mode: CrpMode,
) -> String {
if paths.is_empty() {
return "No files specified.".to_string();
}
let mut candidates: Vec<FileCandidate> = Vec::new();
for path in paths {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => continue,
};
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let tokens_full = count_tokens(&content);
let sigs = signatures::extract_signatures(&content, ext);
let sig_text: String = sigs
.iter()
.map(|s| s.to_compact())
.collect::<Vec<_>>()
.join("\n");
let tokens_sig = count_tokens(&sig_text);
let map_text = format_map(&content, ext, &sigs);
let tokens_map = count_tokens(&map_text);
let score = compute_relevance_score(path, &content);
candidates.push(FileCandidate {
path: path.clone(),
score,
tokens_full,
tokens_map,
tokens_sig,
});
}
candidates.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut used_tokens = 0usize;
let mut selections: Vec<(String, String)> = Vec::new();
for candidate in &candidates {
if used_tokens >= budget {
break;
}
let remaining = budget - used_tokens;
let (mode, cost) = select_best_fit(candidate, remaining);
if cost > remaining {
let sig_cost = candidate.tokens_sig;
if sig_cost <= remaining {
selections.push((candidate.path.clone(), "signatures".to_string()));
used_tokens += sig_cost;
}
continue;
}
selections.push((candidate.path.clone(), mode));
used_tokens += cost;
}
let mut output_parts = Vec::new();
output_parts.push(format!(
"ctx_fill: {budget} token budget, {} files analyzed, {} selected",
candidates.len(),
selections.len()
));
output_parts.push(String::new());
for (path, mode) in &selections {
let result = crate::tools::ctx_read::handle(cache, path, mode, crp_mode);
output_parts.push(result);
output_parts.push("---".to_string());
}
let skipped = candidates.len() - selections.len();
if skipped > 0 {
output_parts.push(format!("{skipped} files skipped (budget exhausted)"));
}
output_parts.push(format!("\nUsed: {used_tokens}/{budget} tokens"));
output_parts.join("\n")
}
fn select_best_fit(candidate: &FileCandidate, remaining: usize) -> (String, usize) {
if candidate.tokens_full <= remaining {
return ("full".to_string(), candidate.tokens_full);
}
if candidate.tokens_map <= remaining {
return ("map".to_string(), candidate.tokens_map);
}
if candidate.tokens_sig <= remaining {
return ("signatures".to_string(), candidate.tokens_sig);
}
("signatures".to_string(), candidate.tokens_sig)
}
fn compute_relevance_score(path: &str, content: &str) -> f64 {
let mut score = 1.0;
let name = Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if name.contains("test") || name.contains("spec") {
score *= 0.5;
}
if name.contains("config") || name.contains("types") || name.contains("schema") {
score *= 1.3;
}
if name == "mod.rs" || name == "index.ts" || name == "index.js" || name == "__init__.py" {
score *= 1.5;
}
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if matches!(ext, "rs" | "ts" | "py" | "go" | "java") {
score *= 1.2;
}
let lines = content.lines().count();
if lines > 500 {
score *= 0.8;
}
if lines < 50 {
score *= 1.1;
}
let export_count = content
.lines()
.filter(|l| l.contains("pub ") || l.contains("export ") || l.contains("def "))
.count();
score *= 1.0 + (export_count as f64 * 0.02).min(0.5);
score
}
fn format_map(content: &str, ext: &str, sigs: &[crate::core::signatures::Signature]) -> String {
let deps = crate::core::deps::extract_deps(content, ext);
let mut parts = Vec::new();
if !deps.imports.is_empty() {
parts.push(format!("deps: {}", deps.imports.join(", ")));
}
if !deps.exports.is_empty() {
parts.push(format!("exports: {}", deps.exports.join(", ")));
}
let key_sigs: Vec<_> = sigs
.iter()
.filter(|s| s.is_exported || s.indent == 0)
.collect();
for sig in &key_sigs {
parts.push(sig.to_compact());
}
parts.join("\n")
}