impl PdcaLoop {
pub fn new() -> Self {
Self {
config: OracleConfig::default(),
targets: ConvergenceTargets::default(),
collector: AggregatedCollector::new(),
}
}
pub fn with_config(config: OracleConfig, targets: ConvergenceTargets) -> Self {
Self {
config,
targets,
collector: AggregatedCollector::new(),
}
}
pub fn max_iterations(&self) -> usize {
self.config.max_iterations
}
pub async fn run(&self, project_path: &Path) -> Result<Vec<PdcaIterationResult>> {
if !project_path.exists() {
bail!("Project path does not exist: {}", project_path.display());
}
let mut results = Vec::new();
let mut stagnation_count = 0;
let mut previous_defect_count = usize::MAX;
for iteration in 1..=self.config.max_iterations {
let metrics_before = self.collect_metrics(project_path).await?;
let signals = self.collector.collect_all(project_path).await?;
let mut defects = self.collector.signals_to_defects(signals);
for defect in &mut defects {
defect.update_decision(
self.config.auto_apply_threshold,
self.config.review_threshold,
);
}
if defects.len() == previous_defect_count {
stagnation_count += 1;
if stagnation_count >= self.config.stagnation_threshold && self.config.andon_enabled
{
eprintln!(
"ANDON: Stagnation detected after {} iterations with {} defects",
stagnation_count,
defects.len()
);
}
} else {
stagnation_count = 0;
}
previous_defect_count = defects.len();
let auto_apply_defects: Vec<_> = defects
.iter()
.filter(|d| d.decision == OracleDecision::AutoApply)
.take(self.config.batch_size)
.collect();
let defects_fixed = self.apply_fixes(&auto_apply_defects, project_path).await?;
let metrics_after = self.collect_metrics(project_path).await?;
if self.config.andon_enabled {
self.check_regression(&metrics_before, &metrics_after)?;
}
let status = self.targets.check(&metrics_after);
let converged = matches!(status, ConvergenceStatus::Converged);
let result = PdcaIterationResult {
iteration,
defects_found: defects.len(),
defects_fixed,
defects_skipped: defects.len() - auto_apply_defects.len(),
metrics_before,
metrics_after,
converged,
};
results.push(result.clone());
if converged {
eprintln!("CONVERGED at iteration {}", iteration);
break;
}
if iteration > 1 {
let progress = self.calculate_progress(&results);
if progress < self.config.min_progress_per_iteration {
eprintln!(
"Insufficient progress ({:.4} < {:.4}), continuing...",
progress, self.config.min_progress_per_iteration
);
}
}
}
Ok(results)
}
pub async fn run_single(&self, project_path: &Path) -> Result<PdcaIterationResult> {
let results = self.run_iterations(project_path, 1).await?;
results
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("No iteration result"))
}
pub async fn run_iterations(
&self,
project_path: &Path,
max_iterations: usize,
) -> Result<Vec<PdcaIterationResult>> {
let limited_config = OracleConfig {
max_iterations,
..self.config.clone()
};
let limited_loop = PdcaLoop {
config: limited_config,
targets: self.targets.clone(),
collector: AggregatedCollector::new(),
};
limited_loop.run(project_path).await
}
async fn collect_metrics(&self, project_path: &Path) -> Result<ProjectMetrics> {
let signals = self.collector.collect_all(project_path).await?;
let compiler_errors = signals
.iter()
.filter(|s| s.source == SignalSource::Rustc)
.count();
let clippy_warnings = signals
.iter()
.filter(|s| s.source == SignalSource::Clippy)
.count();
let test_failures = signals
.iter()
.filter(|s| s.source == SignalSource::CargoTest)
.count();
Ok(ProjectMetrics {
compiler_errors,
clippy_warnings,
test_failures,
..Default::default()
})
}
async fn apply_fixes(&self, defects: &[&DefectReport], project_path: &Path) -> Result<usize> {
let mut fixed_count = 0;
for defect in defects {
for fix in &defect.suggested_fixes {
match &fix.fix_type {
FixType::ClippyAutoFix => {
let output = std::process::Command::new("cargo")
.args(["clippy", "--fix", "--allow-dirty", "--allow-staged"])
.current_dir(project_path)
.output()?;
if output.status.success() {
fixed_count += 1;
}
}
FixType::Replacement { old, new } => {
let content = std::fs::read_to_string(&defect.location.file_path)?;
let updated = content.replace(old, new);
std::fs::write(&defect.location.file_path, updated)?;
fixed_count += 1;
}
_ => {
}
}
}
}
Ok(fixed_count)
}
fn check_regression(&self, before: &ProjectMetrics, after: &ProjectMetrics) -> Result<()> {
if after.test_coverage < before.test_coverage - 0.01 {
bail!(
"ANDON: Coverage decreased: {:.1}% → {:.1}%",
before.test_coverage * 100.0,
after.test_coverage * 100.0
);
}
if after.compiler_errors > before.compiler_errors {
bail!(
"ANDON: Compiler errors increased: {} → {}",
before.compiler_errors,
after.compiler_errors
);
}
if after.test_failures > before.test_failures {
bail!(
"ANDON: Test failures increased: {} → {}",
before.test_failures,
after.test_failures
);
}
Ok(())
}
fn calculate_progress(&self, results: &[PdcaIterationResult]) -> f32 {
if results.len() < 2 {
return 1.0;
}
let first = &results[0];
let last = results.last().expect("internal error");
let initial_defects = first.defects_found as f32;
let current_defects = last.defects_found as f32;
if initial_defects == 0.0 {
return 1.0;
}
(initial_defects - current_defects) / initial_defects
}
}
impl Default for PdcaLoop {
fn default() -> Self {
Self::new()
}
}