//! Patch synthesizer — writes a unified-diff file for `git apply` preview. //! //! This crate NEVER runs git. Per RULE 0.13 the orchestrator is the only //! party that commits. We emit `.patch` text the user reads + applies. //! //! Only items whose resolution == AutoApply are materialised here; the //! zero-conflict guarantee keeps `requires_human_decision` items out. use crate::plan::{Plan, PlanItem, Resolution}; use anyhow::Result; use std::fs; use std::path::Path; pub fn write_patch(plan: &Plan, branch: &str, out_file: &Path) -> Result { let auto = plan.auto_items(); let mut body = String::new(); body.push_str(&header(branch, auto.len(), plan.manual_items().len())); for item in &auto { body.push_str(&hunk_for(item)); } fs::write(out_file, body)?; Ok(auto.len()) } fn header(branch: &str, auto: usize, manual: usize) -> String { format!( "# kei-refactor-engine preview patch\n\ # Branch intent: {branch}\n\ # Auto-apply items: {auto}\n\ # Human-decision items (NOT in this patch, see plan): {manual}\n\ # Review with `git apply --check ` before merging.\n\n" ) } fn hunk_for(item: &PlanItem) -> String { // Conservative: we do not invent file content. We emit an annotated // comment block per item so the user sees intent, not fabricated code. let files = item.files.join(", "); format!( "--- a/{file}\n+++ b/{file}\n# INTENT ({cat}/{sev}): {why}\n# FILES: {files}\n# EXAMPLE: {ex}\n# TRADEOFF: {tr}\n\n", file = item.files.first().cloned().unwrap_or_else(|| "".into()), cat = item.category, sev = item.severity, why = item.why, files = files, ex = item.example, tr = item.tradeoff, ) } pub fn excluded_manual(plan: &Plan) -> Vec<&PlanItem> { plan.items .iter() .filter(|i| i.resolution == Resolution::RequiresHumanDecision) .collect() }