impl CorpusRunner {
fn compute_format_scores(
&self,
results: &[CorpusResult],
registry: &CorpusRegistry,
) -> Vec<FormatScore> {
let mut scores = Vec::new();
let format_by_id: HashMap<&str, CorpusFormat> = registry
.entries
.iter()
.map(|e| (e.id.as_str(), e.format))
.collect();
for format in &[
CorpusFormat::Bash,
CorpusFormat::Makefile,
CorpusFormat::Dockerfile,
] {
let format_results: Vec<&CorpusResult> = results
.iter()
.filter(|r| format_by_id.get(r.id.as_str()) == Some(format))
.collect();
if format_results.is_empty() {
continue;
}
let ft = format_results.len();
let fp = format_results.iter().filter(|r| r.transpiled).count();
let fr = if ft > 0 { fp as f64 / ft as f64 } else { 0.0 };
let fs = if ft > 0 {
let ts: f64 = format_results.iter().map(|r| r.score()).sum();
ts / ft as f64
} else {
0.0
};
scores.push(FormatScore {
format: *format,
total: ft,
passed: fp,
rate: fr,
score: fs,
grade: Grade::from_score(fs),
});
}
scores
}
pub fn convergence_entry(
&self,
score: &CorpusScore,
iteration: u32,
date: &str,
previous_rate: f64,
notes: &str,
) -> ConvergenceEntry {
let (bash_passed, bash_total) = score
.format_score(CorpusFormat::Bash)
.map_or((0, 0), |fs| (fs.passed, fs.total));
let (makefile_passed, makefile_total) = score
.format_score(CorpusFormat::Makefile)
.map_or((0, 0), |fs| (fs.passed, fs.total));
let (dockerfile_passed, dockerfile_total) = score
.format_score(CorpusFormat::Dockerfile)
.map_or((0, 0), |fs| (fs.passed, fs.total));
let bash_score = score
.format_score(CorpusFormat::Bash)
.map_or(0.0, |fs| fs.score);
let makefile_score = score
.format_score(CorpusFormat::Makefile)
.map_or(0.0, |fs| fs.score);
let dockerfile_score = score
.format_score(CorpusFormat::Dockerfile)
.map_or(0.0, |fs| fs.score);
let lint_passed = score.results.iter().filter(|r| r.lint_clean).count();
let lint_rate = if score.total > 0 {
lint_passed as f64 / score.total as f64
} else {
0.0
};
ConvergenceEntry {
iteration,
date: date.to_string(),
total: score.total,
passed: score.passed,
failed: score.failed,
rate: score.rate,
delta: score.rate - previous_rate,
notes: notes.to_string(),
bash_passed,
bash_total,
makefile_passed,
makefile_total,
dockerfile_passed,
dockerfile_total,
score: score.score,
grade: score.grade.to_string(),
bash_score,
makefile_score,
dockerfile_score,
lint_passed,
lint_rate,
}
}
pub fn append_convergence_log(
entry: &ConvergenceEntry,
path: &std::path::Path,
) -> std::io::Result<()> {
use std::io::Write;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let json = serde_json::to_string(entry)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
writeln!(file, "{json}")?;
Ok(())
}
pub fn load_convergence_log(path: &std::path::Path) -> std::io::Result<Vec<ConvergenceEntry>> {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e),
};
let mut entries = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let entry: ConvergenceEntry = serde_json::from_str(trimmed)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
entries.push(entry);
}
Ok(entries)
}
pub fn is_converged(entries: &[ConvergenceEntry]) -> bool {
if entries.len() < 3 {
return false;
}
let last_three = &entries[entries.len() - 3..];
let rate_met = last_three.iter().all(|e| e.rate >= 0.99);
let stable = last_three.iter().all(|e| e.delta.abs() < 0.005);
rate_met && stable
}
}
include!("runner_cont_4.rs");