use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use crate::heal_memory::HealMemory;
use crate::patch_memory::PatchMemory;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrainingExample {
pub fingerprint: String,
pub rel_path: String,
pub content: String,
pub weight: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TensionContext {
pub command: String,
pub ce_mean: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FineTuneReport {
pub examples: usize,
pub corpus_files: usize,
pub final_loss: Option<f32>,
}
pub fn collect_from_patch_memory(pm: &PatchMemory, min_verified: u32) -> Vec<TrainingExample> {
let mut out = Vec::new();
for (fingerprint, candidates) in &pm.by_fingerprint {
for c in candidates {
if c.verified_count >= min_verified.max(1) {
out.push(TrainingExample {
fingerprint: fingerprint.clone(),
rel_path: c.rel_path.clone(),
content: c.content.clone(),
weight: c.verified_count,
});
}
}
}
out.sort_by_key(|e| std::cmp::Reverse(e.weight));
out
}
pub fn collect_tension_context(hm: &HealMemory) -> Vec<TensionContext> {
let mut out: Vec<TensionContext> = hm
.all_records()
.into_iter()
.filter(|r| r.ce_count > 0)
.map(|r| TensionContext {
command: r.command.clone(),
ce_mean: r.ce_mean,
})
.collect();
out.sort_by(|a, b| {
b.ce_mean
.partial_cmp(&a.ce_mean)
.unwrap_or(std::cmp::Ordering::Equal)
});
out
}
pub fn write_corpus(examples: &[TrainingExample], dir: &Path) -> std::io::Result<usize> {
std::fs::create_dir_all(dir)?;
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
let is_generated = path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("hindsight_") && n.ends_with(".rs"))
.unwrap_or(false);
if is_generated {
let _ = std::fs::remove_file(path);
}
}
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut written = 0usize;
for (i, ex) in examples.iter().enumerate() {
if !seen.insert(ex.content.clone()) {
continue; }
let fname = format!("hindsight_{i:06}.rs");
std::fs::write(dir.join(fname), &ex.content)?;
written += 1;
}
Ok(written)
}
pub fn default_corpus_dir() -> PathBuf {
if let Some(pm_path) = PatchMemory::default_path() {
return pm_path.with_file_name("axiom_hindsight_corpus");
}
std::env::temp_dir().join("axiom_hindsight_corpus")
}
#[allow(clippy::too_many_arguments)]
pub fn fine_tune(
pm: &PatchMemory,
min_verified: u32,
corpus_dir: &Path,
config: crate::config::AxiomConfig,
device: candle_core::Device,
checkpoint_path: impl Into<String>,
epochs: usize,
steps_per_epoch: usize,
lr: f64,
) -> candle_core::Result<FineTuneReport> {
let examples = collect_from_patch_memory(pm, min_verified);
if examples.is_empty() {
return Ok(FineTuneReport {
examples: 0,
corpus_files: 0,
final_loss: None,
});
}
let corpus_files = write_corpus(&examples, corpus_dir).map_err(candle_core::Error::wrap)?;
let seq_len = 16usize;
let mut trainer = crate::meta_train::MetaTrainer::build(
config,
device,
corpus_dir,
checkpoint_path,
corpus_files.clamp(1, 4),
seq_len,
4096,
65536,
0xA11CE,
)?;
if trainer.dataset_len() == 0 {
return Ok(FineTuneReport {
examples: examples.len(),
corpus_files,
final_loss: None,
});
}
let final_loss = trainer.run(epochs.max(1), steps_per_epoch.max(1), lr)?;
Ok(FineTuneReport {
examples: examples.len(),
corpus_files,
final_loss: Some(final_loss),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(tag: &str) -> PathBuf {
let n = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("axiom_hindsight_{tag}_{n}"))
}
fn long_fix(seed: &str) -> String {
let mut s = format!("// fix for {seed}\nfn solve_{seed}() -> i32 {{\n");
for i in 0..40 {
s.push_str(&format!(" let v{i} = {i} + {seed}_base();\n"));
}
s.push_str(" 0\n}\n");
s
}
#[test]
fn collect_filters_by_min_verified_and_sorts_by_weight() {
let mut pm = PatchMemory::new();
pm.record_verified("fpA", "src/a.rs", "fn a() {}");
pm.record_verified("fpA", "src/a.rs", "fn a() {}");
pm.record_verified("fpA", "src/a.rs", "fn a() {}");
pm.record_verified("fpB", "src/b.rs", "fn b() {}");
let all = collect_from_patch_memory(&pm, 1);
assert_eq!(all.len(), 2);
assert_eq!(all[0].weight, 3, "most-confirmed example must lead");
assert_eq!(all[0].rel_path, "src/a.rs");
let strong_only = collect_from_patch_memory(&pm, 2);
assert_eq!(strong_only.len(), 1, "min_verified must filter weak fixes");
assert_eq!(strong_only[0].fingerprint, "fpA");
}
#[test]
fn write_corpus_dedups_identical_contents() {
let dir = tmp("corpus");
let examples = vec![
TrainingExample {
fingerprint: "fp".into(),
rel_path: "a.rs".into(),
content: "fn a() {}".into(),
weight: 2,
},
TrainingExample {
fingerprint: "fp".into(),
rel_path: "a.rs".into(),
content: "fn a() {}".into(), weight: 1,
},
TrainingExample {
fingerprint: "fp2".into(),
rel_path: "b.rs".into(),
content: "fn b() {}".into(),
weight: 1,
},
];
let n = write_corpus(&examples, &dir).unwrap();
assert_eq!(n, 2, "identical contents must be written once");
let count = std::fs::read_dir(&dir).unwrap().count();
assert_eq!(count, 2);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_corpus_prunes_stale_generated_files() {
let dir = tmp("prune");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("hindsight_999999.rs"), "stale").unwrap();
std::fs::write(dir.join("keep.txt"), "unrelated").unwrap();
let examples = vec![TrainingExample {
fingerprint: "fp".into(),
rel_path: "a.rs".into(),
content: "fn a() {}".into(),
weight: 1,
}];
let n = write_corpus(&examples, &dir).unwrap();
assert_eq!(n, 1);
assert!(!dir.join("hindsight_999999.rs").exists(), "stale file must be pruned");
assert!(dir.join("keep.txt").exists(), "non-generated files must be left alone");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fine_tune_is_a_noop_with_no_qualifying_examples() {
let pm = PatchMemory::new();
let dir = tmp("noop");
let report = fine_tune(
&pm,
1,
&dir,
crate::config::AxiomConfig::runtime_small(),
candle_core::Device::Cpu,
dir.join("ckpt.safetensors").to_string_lossy().to_string(),
1,
1,
1e-3,
)
.unwrap();
assert_eq!(report.examples, 0);
assert!(report.final_loss.is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn fine_tune_trains_end_to_end_on_verified_fixes() {
let mut pm = PatchMemory::new();
pm.record_verified("fp1", "src/one.rs", &long_fix("one"));
pm.record_verified("fp2", "src/two.rs", &long_fix("two"));
let dir = tmp("train");
let ckpt = dir.join("ckpt.safetensors").to_string_lossy().to_string();
let report = fine_tune(
&pm,
1,
&dir.join("corpus"),
crate::config::AxiomConfig::runtime_small(),
candle_core::Device::Cpu,
ckpt.clone(),
1,
2,
1e-3,
)
.unwrap();
assert_eq!(report.examples, 2);
assert!(report.corpus_files >= 2);
let loss = report.final_loss.expect("should have trained");
assert!(loss.is_finite() && loss >= 0.0, "loss must be finite (got {loss})");
assert!(Path::new(&ckpt).exists(), "checkpoint must be written");
let _ = std::fs::remove_dir_all(&dir);
}
}