use super::ast_tools;
use super::tournament::Hypothesis;
use std::path::{Path, PathBuf};
pub fn is_micro_model(model_name: &str) -> bool {
let name_lower = model_name.to_lowercase();
if name_lower.contains("tiny") || name_lower.contains("small") {
return true;
}
let micro_sizes = ["0.8b", "1.5b", "1b", "2b", "3b", "4b"];
for size in µ_sizes {
let mut search_from = 0;
while search_from < name_lower.len() {
if let Some(pos) = name_lower[search_from..].find(size) {
let abs_pos = search_from + pos;
let end_pos = abs_pos + size.len();
let prev_ok =
abs_pos == 0 || !name_lower.as_bytes()[abs_pos - 1].is_ascii_alphanumeric();
let next_ok = end_pos >= name_lower.len()
|| !name_lower.as_bytes()[end_pos].is_ascii_alphanumeric();
if prev_ok && next_ok {
return true;
}
search_from = abs_pos + 1;
} else {
break;
}
}
}
false
}
pub const MICRO_MAX_CONTEXT_CHARS: usize = 12_000;
pub const MICRO_MAX_FILES: usize = 3;
pub const MICRO_MAX_EDITS: usize = 2;
pub fn build_micro_system_prompt(population_size: usize) -> String {
let n = population_size.min(3);
format!(
r#"You are a code improvement engine. Generate {n} code mutations.
FORMAT:
Return a JSON array like this:
[{{"description": "fix off-by-one error", "edits": [{{"file": "src/foo.rs", "search": "for i in 0..n", "replace": "for i in 0..=n"}}], "target_files": ["src/foo.rs"]}}]
RULES:
1. "search" must be EXACT text from the file (copy-paste)
2. Only modify files shown in the context
3. Never modify src/evolution/, src/safety/, or benches/
4. Keep edits small - change only what's needed
/no_think"#
)
}
pub fn select_micro_targets(all_targets: &[PathBuf], repo_root: &Path) -> Vec<(PathBuf, String)> {
let files_with_content: Vec<(PathBuf, usize, String)> = all_targets
.iter()
.filter_map(|p| {
let full_path = repo_root.join(p);
let size = std::fs::metadata(&full_path)
.map(|m| m.len() as usize)
.unwrap_or(usize::MAX);
let content = std::fs::read_to_string(&full_path).ok()?;
Some((p.clone(), size, content))
})
.collect();
let mut files_sorted = files_with_content;
files_sorted.sort_by_key(|(_, size, _)| *size);
let mut selected = Vec::new();
let mut total_chars = 0usize;
for (path, _size, content) in files_sorted.into_iter().take(MICRO_MAX_FILES) {
if total_chars + content.len() > MICRO_MAX_CONTEXT_CHARS {
break;
}
total_chars += content.len();
selected.push((path, content));
}
selected
}
pub fn build_micro_context(targets: &[(PathBuf, String)]) -> String {
let mut context = String::with_capacity(MICRO_MAX_CONTEXT_CHARS);
for (path, content) in targets {
let numbered: String = content
.lines()
.enumerate()
.map(|(i, line)| format!("{:3}| {}\n", i + 1, line))
.collect();
context.push_str(&format!("\n=== {} ===\n{}", path.display(), numbered));
}
context
}
pub fn validate_micro_hypothesis(hypothesis: &Hypothesis) -> Result<(), String> {
let edits: Vec<serde_json::Value> = match serde_json::from_str(&hypothesis.patch) {
Ok(v) => v,
Err(_) => {
return Ok(());
}
};
if edits.len() > MICRO_MAX_EDITS {
return Err(format!(
"Micro mode: hypothesis has {} edits, max is {}",
edits.len(),
MICRO_MAX_EDITS
));
}
for (i, edit) in edits.iter().enumerate() {
let search = edit["search"].as_str().unwrap_or("");
let replace = edit["replace"].as_str().unwrap_or("");
if search.len() > 500 {
return Err(format!(
"Micro mode: edit {} search too long ({} chars, max 500)",
i,
search.len()
));
}
if replace.len() > 1000 {
return Err(format!(
"Micro mode: edit {} replace too long ({} chars, max 1000)",
i,
replace.len()
));
}
}
Ok(())
}
fn patch_len(hypothesis: &Hypothesis) -> usize {
hypothesis.patch.len()
}
#[cfg(test)]
#[path = "../../tests/unit/evolution/micro_mode/micro_mode_test.rs"]
mod tests;