use std::path::Path;
use crate::{
ApiClient, ApiRequest, AssistantEvent, ContentBlock, ConversationMessage, MessageRole,
};
use serde_json::json;
use crate::api::OllamaApiClient;
use crate::test_runner::{
check_python_imports, has_python_tests, has_rust_tests, run_js_syntax_check,
run_python_syntax_check, run_python_unittest, run_rust_syntax_check, run_ts_syntax_check,
CommandResult,
};
fn coder_num_ctx() -> u32 {
crate::model_config::active().coder.num_ctx
}
fn coder_num_predict() -> u32 {
crate::model_config::active().coder.num_predict
}
const MAX_FIX_ATTEMPTS: u32 = 3;
#[must_use]
pub fn coder_model() -> String {
crate::model_config::active().coder.model
}
#[must_use]
pub fn validation_enabled() -> bool {
std::env::var("CLAUDETTE_VALIDATE_CODE").map_or(true, |v| {
!matches!(v.to_lowercase().as_str(), "false" | "0" | "no" | "off")
})
}
#[derive(Debug, Clone)]
pub struct CodetResult {
pub syntax_ok: bool,
pub tests_found: bool,
pub tests_passed: u32,
pub tests_failed: u32,
pub tests_errors: u32,
pub fixes_applied: u32,
pub attempts_made: u32,
pub fix_summary: String,
pub status: CodetStatus,
}
#[derive(Debug, Clone)]
pub struct ReferenceFile {
pub path: String,
pub content: String,
}
fn format_reference_block(references: &[ReferenceFile]) -> String {
if references.is_empty() {
return String::new();
}
let mut s = String::from(
"\n\n## Reference files (read before writing code)\n\
These existing files are the ground truth. Use ONLY the class names, \
method names, and signatures that actually appear below — do not \
invent or rename any API.\n\n",
);
use std::fmt::Write as _;
for rf in references {
let _ = write!(s, "### `{}`\n```\n{}\n```\n\n", rf.path, rf.content);
}
s
}
#[derive(Debug, Clone)]
pub enum CodetStatus {
AllPassed,
FixedAll,
CouldNotFix { last_error: String },
Skipped,
}
impl CodetResult {
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
json!({
"syntax_ok": self.syntax_ok,
"tests_found": self.tests_found,
"tests_passed": self.tests_passed,
"tests_failed": self.tests_failed,
"fixes_applied": self.fixes_applied,
"attempts_made": self.attempts_made,
"fix_summary": self.fix_summary,
"status": match &self.status {
CodetStatus::AllPassed => "all_passed".to_string(),
CodetStatus::FixedAll => "fixed_all".to_string(),
CodetStatus::CouldNotFix { last_error } => format!("could_not_fix: {last_error}"),
CodetStatus::Skipped => "skipped".to_string(),
},
})
}
}
#[must_use]
pub fn validate_code_file(path: &Path, references: &[ReferenceFile]) -> Option<CodetResult> {
if !validation_enabled() {
return None;
}
let ext = path.extension()?.to_str()?;
let validator: fn(&Path, &[ReferenceFile]) -> CodetResult = match ext {
"py" => validate_python,
"rs" => validate_rust,
"js" | "mjs" | "cjs" => validate_js,
"ts" | "tsx" => validate_ts,
_ => return None,
};
let _swap = CoderSwapGuard::begin(&coder_model());
Some(validator(path, references))
}
struct CoderSwapGuard {
needs_swap: bool,
}
const COALESCE_WINDOW: std::time::Duration = std::time::Duration::from_millis(750);
#[derive(Default)]
struct LeaseState {
coder_model: Option<String>,
active_count: usize,
generation: u64,
}
static LEASE: std::sync::Mutex<LeaseState> = std::sync::Mutex::new(LeaseState {
coder_model: None,
active_count: 0,
generation: 0,
});
impl CoderSwapGuard {
fn begin(coder_model: &str) -> Self {
let brain_model = crate::model_config::active().brain.model.clone();
let needs_swap = crate::brain_selector::should_swap_for_coder(&brain_model, coder_model);
if !needs_swap {
return Self { needs_swap: false };
}
let mut state = LEASE.lock().expect("CODER_LEASE poisoned");
let reusing_same_coder = state.coder_model.as_deref() == Some(coder_model);
if !reusing_same_coder {
if let Some(prev) = state.coder_model.take() {
crate::brain_selector::evict_coder_after_codet(&prev);
}
eprintln!(
" {} {}",
crate::theme::dim("\u{21bb}"),
crate::theme::dim(&format!(
"codet: swapping brain ({brain_model}) \u{2192} coder ({coder_model})..."
)),
);
crate::brain_selector::evict_brain_for_codet(&brain_model);
state.coder_model = Some(coder_model.to_string());
}
state.active_count += 1;
state.generation += 1;
Self { needs_swap: true }
}
}
impl Drop for CoderSwapGuard {
fn drop(&mut self) {
if !self.needs_swap {
return;
}
let scheduled_gen = {
let mut state = LEASE.lock().expect("CODER_LEASE poisoned");
state.active_count = state.active_count.saturating_sub(1);
state.generation += 1;
if state.active_count > 0 {
return;
}
state.generation
};
std::thread::spawn(move || {
std::thread::sleep(COALESCE_WINDOW);
let to_evict = {
let mut state = LEASE.lock().expect("CODER_LEASE poisoned");
if state.generation != scheduled_gen || state.active_count > 0 {
None
} else {
state.coder_model.take()
}
};
if let Some(coder) = to_evict {
crate::brain_selector::evict_coder_after_codet(&coder);
}
});
}
}
pub fn drain_pending_coder_lease() -> bool {
let to_evict = {
let Ok(mut state) = LEASE.lock() else {
return false;
};
if state.active_count > 0 {
return false;
}
state.coder_model.take()
};
if let Some(coder) = to_evict {
crate::brain_selector::evict_coder_after_codet(&coder);
true
} else {
false
}
}
fn toolchain_missing(result: &CommandResult) -> bool {
!result.timed_out && result.exit_code.is_none() && result.stderr.contains("failed to spawn")
}
fn skipped_result(msg: &str) -> CodetResult {
CodetResult {
syntax_ok: true,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied: 0,
attempts_made: 0,
fix_summary: msg.to_string(),
status: CodetStatus::Skipped,
}
}
fn rust_failure_is_syntax_error(stderr: &str) -> bool {
const PARSE_MARKERS: &[&str] = &[
"expected",
"unclosed",
"unexpected",
"unterminated",
"mismatched closing delimiter",
"unknown start of token",
"unknown character",
"this file contains an un",
];
stderr.lines().any(|line| {
line.trim_start()
.strip_prefix("error:")
.map(str::to_ascii_lowercase)
.is_some_and(|rest| PARSE_MARKERS.iter().any(|m| rest.contains(m)))
})
}
fn rust_syntax_check_ok(result: &CommandResult) -> bool {
result.success
|| toolchain_missing(result)
|| !rust_failure_is_syntax_error(&format!("{}\n{}", result.stdout, result.stderr))
}
fn validate_python(path: &Path, references: &[ReferenceFile]) -> CodetResult {
let mut fixes_applied: u32 = 0;
let mut attempts_made: u32 = 0;
let mut fix_descriptions: Vec<String> = Vec::new();
let syntax = run_python_syntax_check(path);
if toolchain_missing(&syntax) {
return skipped_result("python not available, syntax check skipped");
}
if !syntax.success {
let content = std::fs::read_to_string(path).unwrap_or_default();
let error_msg = format!("{}\n{}", syntax.stdout, syntax.stderr);
match try_fix_loop(path, &content, &error_msg, FixTarget::Syntax, references) {
FixLoopOutcome::Fixed {
description,
attempts_tried,
} => {
fixes_applied += 1;
attempts_made += attempts_tried;
fix_descriptions.push(description);
}
FixLoopOutcome::GaveUp {
last_error,
attempts_tried,
} => {
attempts_made += attempts_tried;
return CodetResult {
syntax_ok: false,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix { last_error },
};
}
}
}
let content = std::fs::read_to_string(path).unwrap_or_default();
if !has_python_tests(&content) {
return CodetResult {
syntax_ok: true,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if fixes_applied > 0 {
CodetStatus::FixedAll
} else {
CodetStatus::AllPassed
},
};
}
let imports = check_python_imports(path);
if !imports.missing.is_empty() {
return CodetResult {
syntax_ok: true,
tests_found: true,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix {
last_error: format!(
"cannot validate tests — Python package(s) not importable: {}. \
Install them in the active Python environment and retry with /validate.",
imports.missing.join(", "),
),
},
};
}
let test_result = run_python_unittest(path);
if test_result.all_passed {
return CodetResult {
syntax_ok: true,
tests_found: true,
tests_passed: test_result.passed,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if fixes_applied > 0 {
CodetStatus::FixedAll
} else {
CodetStatus::AllPassed
},
};
}
match try_fix_loop(
path,
&content,
&test_result.error_output,
FixTarget::Tests,
references,
) {
FixLoopOutcome::Fixed {
description,
attempts_tried,
} => {
fixes_applied += 1;
attempts_made += attempts_tried;
fix_descriptions.push(description);
let final_tests = run_python_unittest(path);
CodetResult {
syntax_ok: true,
tests_found: true,
tests_passed: final_tests.passed,
tests_failed: final_tests.failed,
tests_errors: final_tests.errors,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if final_tests.all_passed {
CodetStatus::FixedAll
} else {
CodetStatus::CouldNotFix {
last_error: final_tests.error_output,
}
},
}
}
FixLoopOutcome::GaveUp {
last_error,
attempts_tried,
} => {
attempts_made += attempts_tried;
CodetResult {
syntax_ok: true,
tests_found: true,
tests_passed: test_result.passed,
tests_failed: test_result.failed,
tests_errors: test_result.errors,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix { last_error },
}
}
}
}
fn validate_rust(path: &Path, references: &[ReferenceFile]) -> CodetResult {
let mut fixes_applied: u32 = 0;
let mut attempts_made: u32 = 0;
let mut fix_descriptions: Vec<String> = Vec::new();
let syntax = run_rust_syntax_check(path);
if toolchain_missing(&syntax) {
return skipped_result("rustc not available, syntax check skipped");
}
if !rust_syntax_check_ok(&syntax) {
let content = std::fs::read_to_string(path).unwrap_or_default();
let error_msg = format!("{}\n{}", syntax.stdout, syntax.stderr);
match try_fix_loop(
path,
&content,
&error_msg,
FixTarget::RustSyntax,
references,
) {
FixLoopOutcome::Fixed {
description,
attempts_tried,
} => {
fixes_applied += 1;
attempts_made += attempts_tried;
fix_descriptions.push(description);
}
FixLoopOutcome::GaveUp {
last_error,
attempts_tried,
} => {
attempts_made += attempts_tried;
return CodetResult {
syntax_ok: false,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix { last_error },
};
}
}
}
let content = std::fs::read_to_string(path).unwrap_or_default();
CodetResult {
syntax_ok: true,
tests_found: has_rust_tests(&content),
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if fixes_applied > 0 {
CodetStatus::FixedAll
} else {
CodetStatus::AllPassed
},
}
}
fn validate_js(path: &Path, references: &[ReferenceFile]) -> CodetResult {
let mut fixes_applied: u32 = 0;
let mut attempts_made: u32 = 0;
let mut fix_descriptions: Vec<String> = Vec::new();
let syntax = run_js_syntax_check(path);
if toolchain_missing(&syntax) {
return skipped_result("node not available, syntax check skipped");
}
if !syntax.success {
let content = std::fs::read_to_string(path).unwrap_or_default();
let error_msg = format!("{}\n{}", syntax.stdout, syntax.stderr);
match try_fix_loop(path, &content, &error_msg, FixTarget::JsSyntax, references) {
FixLoopOutcome::Fixed {
description,
attempts_tried,
} => {
fixes_applied += 1;
attempts_made += attempts_tried;
fix_descriptions.push(description);
}
FixLoopOutcome::GaveUp {
last_error,
attempts_tried,
} => {
attempts_made += attempts_tried;
return CodetResult {
syntax_ok: false,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix { last_error },
};
}
}
}
CodetResult {
syntax_ok: true,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if fixes_applied > 0 {
CodetStatus::FixedAll
} else {
CodetStatus::AllPassed
},
}
}
fn validate_ts(path: &Path, references: &[ReferenceFile]) -> CodetResult {
let mut fixes_applied: u32 = 0;
let mut attempts_made: u32 = 0;
let mut fix_descriptions: Vec<String> = Vec::new();
let syntax = run_ts_syntax_check(path);
if !syntax.success {
if toolchain_missing(&syntax)
|| syntax.stderr.contains("not found")
|| syntax.stderr.contains("not recognized")
{
return skipped_result("tsc not available, syntax check skipped");
}
let content = std::fs::read_to_string(path).unwrap_or_default();
let error_msg = format!("{}\n{}", syntax.stdout, syntax.stderr);
match try_fix_loop(path, &content, &error_msg, FixTarget::TsSyntax, references) {
FixLoopOutcome::Fixed {
description,
attempts_tried,
} => {
fixes_applied += 1;
attempts_made += attempts_tried;
fix_descriptions.push(description);
}
FixLoopOutcome::GaveUp {
last_error,
attempts_tried,
} => {
attempts_made += attempts_tried;
return CodetResult {
syntax_ok: false,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: CodetStatus::CouldNotFix { last_error },
};
}
}
}
CodetResult {
syntax_ok: true,
tests_found: false,
tests_passed: 0,
tests_failed: 0,
tests_errors: 0,
fixes_applied,
attempts_made,
fix_summary: fix_descriptions.join("; "),
status: if fixes_applied > 0 {
CodetStatus::FixedAll
} else {
CodetStatus::AllPassed
},
}
}
#[derive(Debug, Clone, Copy)]
enum FixTarget {
Syntax,
Tests,
RustSyntax,
JsSyntax,
TsSyntax,
}
enum FixLoopOutcome {
Fixed {
description: String,
attempts_tried: u32,
},
GaveUp {
last_error: String,
attempts_tried: u32,
},
}
#[derive(Debug, Clone)]
struct Patch {
search: String,
replace: String,
}
fn try_fix_loop(
path: &Path,
original_content: &str,
initial_error: &str,
target: FixTarget,
references: &[ReferenceFile],
) -> FixLoopOutcome {
let mut current_content = original_content.to_string();
let mut last_error = initial_error.to_string();
let use_surgical_path = matches!(
target,
FixTarget::Syntax | FixTarget::RustSyntax | FixTarget::JsSyntax | FixTarget::TsSyntax
);
for attempt in 0..MAX_FIX_ATTEMPTS {
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!(
"codet: fix attempt {}/{MAX_FIX_ATTEMPTS}",
attempt + 1
)),
);
let fixed_content = if use_surgical_path {
ask_coder_for_patches(¤t_content, &last_error, references)
.and_then(|patches| apply_patches(¤t_content, &patches))
.or_else(|| {
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(
"codet: surgical path failed, falling back to full regen...",
),
);
ask_coder_to_fix(¤t_content, &last_error, references)
})
} else {
ask_coder_to_fix(¤t_content, &last_error, references)
};
let Some(fixed_content) = fixed_content else {
eprintln!(
" {} {}",
crate::theme::warn(crate::theme::WARN_GLYPH),
crate::theme::warn("codet: coder returned no usable fix, retrying..."),
);
continue;
};
if std::fs::write(path, &fixed_content).is_err() {
continue;
}
let improved = match target {
FixTarget::Syntax => run_python_syntax_check(path).success,
FixTarget::RustSyntax => rust_syntax_check_ok(&run_rust_syntax_check(path)),
FixTarget::JsSyntax => run_js_syntax_check(path).success,
FixTarget::TsSyntax => run_ts_syntax_check(path).success,
FixTarget::Tests => {
let recheck = run_python_unittest(path);
recheck.all_passed
|| (recheck.failed + recheck.errors) < (count_failures_from_error(&last_error))
}
};
if improved {
let desc = match target {
FixTarget::Syntax => "fixed Python syntax error".to_string(),
FixTarget::RustSyntax => "fixed Rust syntax error".to_string(),
FixTarget::JsSyntax => "fixed JavaScript syntax error".to_string(),
FixTarget::TsSyntax => "fixed TypeScript syntax error".to_string(),
FixTarget::Tests => "fixed failing test(s)".to_string(),
};
eprintln!(
" {} {}",
crate::theme::ok(crate::theme::OK_GLYPH),
crate::theme::ok(&format!("codet: {desc}")),
);
return FixLoopOutcome::Fixed {
description: desc,
attempts_tried: attempt + 1,
};
}
eprintln!(
" {} {}",
crate::theme::warn(crate::theme::WARN_GLYPH),
crate::theme::warn("codet: fix didn't improve results, retrying..."),
);
let _ = std::fs::write(path, ¤t_content);
current_content = fixed_content;
let retest_err = match target {
FixTarget::Syntax => {
let r = run_python_syntax_check(path);
format!("{}\n{}", r.stdout, r.stderr)
}
FixTarget::RustSyntax => {
let r = run_rust_syntax_check(path);
format!("{}\n{}", r.stdout, r.stderr)
}
FixTarget::JsSyntax => {
let r = run_js_syntax_check(path);
format!("{}\n{}", r.stdout, r.stderr)
}
FixTarget::TsSyntax => {
let r = run_ts_syntax_check(path);
format!("{}\n{}", r.stdout, r.stderr)
}
FixTarget::Tests => {
let r = run_python_unittest(path);
r.error_output
}
};
if !retest_err.trim().is_empty() {
last_error = retest_err;
}
}
let _ = std::fs::write(path, original_content);
FixLoopOutcome::GaveUp {
last_error,
attempts_tried: MAX_FIX_ATTEMPTS,
}
}
fn count_failures_from_error(error: &str) -> u32 {
let mut count = 0u32;
for line in error.lines() {
let trimmed = line.trim_end();
if trimmed.ends_with("... FAIL") || trimmed.ends_with("... ERROR") {
count += 1;
}
}
if count == 0 && !error.trim().is_empty() {
1 } else {
count
}
}
pub fn generate_code(
description: &str,
language: &str,
references: &[ReferenceFile],
) -> Option<String> {
let model = coder_model();
let _swap = CoderSwapGuard::begin(&model);
if !references.is_empty() {
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!(
"codet: generating {language} code via {model} with {} reference file(s)...",
references.len()
)),
);
} else {
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!("codet: generating {language} code via {model}...")),
);
}
let mut client = OllamaApiClient::new(model.clone(), json!([]))
.with_context(coder_num_ctx())
.with_max_predict(coder_num_predict());
let reference_block = format_reference_block(references);
let prompt = format!(
"Write a {language} file matching this description:\n\n\
{description}{reference_block}\n\n\
Requirements:\n\
- Write clean, well-structured, production-quality code\n\
- Include proper comments where the logic isn't obvious\n\
- If the description mentions tests, include them in the same file\n\
- If reference files are provided, use ONLY the real class/method \
names from them — never invent or rename APIs\n\
- Output ONLY the file content — no explanations, no markdown fences"
);
let request = ApiRequest {
system_prompt: vec![format!(
"You are an expert {language} developer. Output ONLY code. \
No explanations, no markdown, no commentary."
)],
messages: vec![ConversationMessage {
role: MessageRole::User,
blocks: vec![ContentBlock::Text { text: prompt }],
usage: None,
}]
.into(),
};
let events = match client.stream(&request) {
Ok(ev) => ev,
Err(e) => {
eprintln!(
" {} {}",
crate::theme::error(crate::theme::ERR_GLYPH),
crate::theme::error(&format!("codet: {model} request failed: {e}")),
);
return None;
}
};
let mut code = String::new();
for event in events {
if let AssistantEvent::TextDelta(text) = event {
code.push_str(&text);
}
}
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!(
"codet: generated {} chars of {language}",
code.len()
)),
);
let code = strip_code_blocks(&code);
if code.trim().is_empty() {
eprintln!(
" {} {}",
crate::theme::warn(crate::theme::WARN_GLYPH),
crate::theme::warn("codet: coder returned empty response"),
);
return None;
}
Some(code)
}
fn ask_coder_to_fix(
file_content: &str,
error_output: &str,
references: &[ReferenceFile],
) -> Option<String> {
let model = coder_model();
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!("codet: asking {model} to fix...")),
);
let mut client = OllamaApiClient::new(model.clone(), json!([]))
.with_context(coder_num_ctx())
.with_max_predict(coder_num_predict());
let reference_block = format_reference_block(references);
let prompt = format!(
"The following Python file has bug(s). Fix them.\n\n\
## File content\n```python\n{file_content}\n```\n\n\
## Error output\n```\n{error_output}\n```{reference_block}\n\n\
Output ONLY the corrected Python file. No explanations, no markdown fences. \
If reference files are provided above, use ONLY the real class/method names \
from them."
);
let request = ApiRequest {
system_prompt: vec![
"You are a Python code fixer. Output ONLY the corrected code. \
No explanations, no markdown, no commentary."
.to_string(),
],
messages: vec![ConversationMessage {
role: MessageRole::User,
blocks: vec![ContentBlock::Text { text: prompt }],
usage: None,
}]
.into(),
};
let events = match client.stream(&request) {
Ok(ev) => ev,
Err(e) => {
eprintln!(
" {} {}",
crate::theme::error(crate::theme::ERR_GLYPH),
crate::theme::error(&format!("codet: {model} request failed: {e}")),
);
return None;
}
};
let mut code = String::new();
for event in events {
if let AssistantEvent::TextDelta(text) = event {
code.push_str(&text);
}
}
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!("codet: got {} chars of response", code.len())),
);
let code = strip_code_blocks(&code);
if code.trim().is_empty() {
eprintln!(
" {} {}",
crate::theme::warn(crate::theme::WARN_GLYPH),
crate::theme::warn("codet: response was empty after stripping fences"),
);
return None;
}
Some(code)
}
fn ask_coder_for_patches(
file_content: &str,
error_output: &str,
references: &[ReferenceFile],
) -> Option<Vec<Patch>> {
let model = coder_model();
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!("codet: asking {model} for surgical patches...")),
);
let mut client = OllamaApiClient::new(model.clone(), json!([]))
.with_context(coder_num_ctx())
.with_max_predict(coder_num_predict());
let reference_block = format_reference_block(references);
let prompt = format!(
"The following file has bug(s). Fix them by emitting SEARCH/REPLACE blocks.\n\n\
## File content\n```\n{file_content}\n```\n\n\
## Error output\n```\n{error_output}\n```{reference_block}\n\n\
## Output format\n\
For each bug, emit EXACTLY this format (no markdown fences around the blocks):\n\n\
<<<<<<< SEARCH\n\
[text from the file to find — must match EXACTLY, whitespace included]\n\
=======\n\
[replacement text]\n\
>>>>>>> REPLACE\n\n\
Rules:\n\
- SEARCH must match the file character-for-character (whitespace, newlines, indentation)\n\
- Include JUST ENOUGH context so SEARCH is unique in the file — don't copy whole functions\n\
- Emit one block per distinct bug; multiple blocks are fine\n\
- NO commentary, NO explanations, NO markdown fences — only the blocks themselves"
);
let request = ApiRequest {
system_prompt: vec![
"You output SEARCH/REPLACE patch blocks only. No prose, no commentary, no fences."
.to_string(),
],
messages: vec![ConversationMessage {
role: MessageRole::User,
blocks: vec![ContentBlock::Text { text: prompt }],
usage: None,
}]
.into(),
};
let events = match client.stream(&request) {
Ok(ev) => ev,
Err(e) => {
eprintln!(
" {} {}",
crate::theme::error(crate::theme::ERR_GLYPH),
crate::theme::error(&format!("codet: {model} patch request failed: {e}")),
);
return None;
}
};
let mut response = String::new();
for event in events {
if let AssistantEvent::TextDelta(text) = event {
response.push_str(&text);
}
}
let patches = parse_search_replace_blocks(&response);
if patches.is_empty() {
eprintln!(
" {} {}",
crate::theme::warn(crate::theme::WARN_GLYPH),
crate::theme::warn(&format!(
"codet: no valid SEARCH/REPLACE blocks in {} chars of response",
response.len()
)),
);
return None;
}
eprintln!(
" {} {}",
crate::theme::dim("▸"),
crate::theme::dim(&format!(
"codet: parsed {} surgical block(s) from {} chars",
patches.len(),
response.len()
)),
);
Some(patches)
}
fn parse_search_replace_blocks(response: &str) -> Vec<Patch> {
const SEARCH_MARK: &str = "<<<<<<< SEARCH";
const SEP_MARK: &str = "=======";
const REPLACE_MARK: &str = ">>>>>>> REPLACE";
let mut patches = Vec::new();
let mut cursor = 0usize;
while cursor < response.len() {
let Some(search_rel) = response[cursor..].find(SEARCH_MARK) else {
break;
};
let search_start = cursor + search_rel + SEARCH_MARK.len();
let Some(sep_rel) = response[search_start..].find(SEP_MARK) else {
break;
};
let sep_start = search_start + sep_rel;
let replace_content_start = sep_start + SEP_MARK.len();
let Some(end_rel) = response[replace_content_start..].find(REPLACE_MARK) else {
break;
};
let replace_end = replace_content_start + end_rel;
let search = response[search_start..sep_start]
.trim_start_matches(['\r', '\n'])
.trim_end_matches(['\r', '\n'])
.to_string();
let replace = response[replace_content_start..replace_end]
.trim_start_matches(['\r', '\n'])
.trim_end_matches(['\r', '\n'])
.to_string();
if !search.is_empty() {
patches.push(Patch { search, replace });
}
cursor = replace_end + REPLACE_MARK.len();
}
patches
}
fn apply_patches(content: &str, patches: &[Patch]) -> Option<String> {
let mut result = content.to_string();
for patch in patches {
if let Some(idx) = find_unique(&result, &patch.search) {
result = format!(
"{}{}{}",
&result[..idx],
patch.replace,
&result[idx + patch.search.len()..]
);
continue;
}
if let Some((start, end)) = fuzzy_find(&result, &patch.search) {
result = format!("{}{}{}", &result[..start], patch.replace, &result[end..]);
continue;
}
return None;
}
Some(result)
}
fn find_unique(haystack: &str, needle: &str) -> Option<usize> {
let first = haystack.find(needle)?;
let second = haystack[first + needle.len()..].find(needle);
if second.is_some() {
None
} else {
Some(first)
}
}
fn fuzzy_find(haystack: &str, needle: &str) -> Option<(usize, usize)> {
let norm = |s: &str| -> String { s.lines().map(str::trim_end).collect::<Vec<_>>().join("\n") };
let normalized_haystack = norm(haystack);
let normalized_needle = norm(needle);
if normalized_needle.is_empty() {
return None;
}
let first = normalized_haystack.find(&normalized_needle)?;
let second = normalized_haystack[first + normalized_needle.len()..].find(&normalized_needle);
if second.is_some() {
return None;
}
let needle_lines: Vec<&str> = normalized_needle.lines().collect();
if needle_lines.is_empty() {
return None;
}
let haystack_lines: Vec<&str> = haystack.split_inclusive('\n').collect();
let mut line_offsets: Vec<usize> = Vec::with_capacity(haystack_lines.len() + 1);
let mut off = 0usize;
for line in &haystack_lines {
line_offsets.push(off);
off += line.len();
}
line_offsets.push(off);
for i in 0..haystack_lines.len() {
if i + needle_lines.len() > haystack_lines.len() {
break;
}
let matched = needle_lines
.iter()
.enumerate()
.all(|(j, nline)| haystack_lines[i + j].trim_end() == *nline);
if !matched {
continue;
}
let last = i + needle_lines.len() - 1;
let last_line = haystack_lines[last];
let eol_len = last_line.len() - last_line.trim_end_matches(['\r', '\n']).len();
let start = line_offsets[i];
let end = (line_offsets[last] + last_line.len()).saturating_sub(eol_len);
return Some((start, end.min(haystack.len())));
}
None
}
fn strip_code_blocks(s: &str) -> String {
let trimmed = s.trim();
if !trimmed.contains("```") {
return trimmed.to_string();
}
let Some(open_pos) = find_line_start_fence(trimmed, 0) else {
return trimmed.to_string();
};
let after_open = &trimmed[open_pos + 3..];
let code_start = match after_open.find('\n') {
Some(nl) => open_pos + 3 + nl + 1,
None => {
return after_open.trim().to_string();
}
};
if let Some(close_pos) = find_line_start_fence(trimmed, code_start) {
return trimmed[code_start..close_pos]
.trim_end_matches('\n')
.to_string();
}
trimmed[code_start..].trim().to_string()
}
fn find_line_start_fence(s: &str, from: usize) -> Option<usize> {
if from == 0 && s.starts_with("```") {
return Some(0);
}
s[from..]
.match_indices("\n```")
.next()
.map(|(i, _)| from + i + 1)
}
#[cfg(test)]
mod tests {
use super::*;
fn cmd_result(
success: bool,
stderr: &str,
exit_code: Option<i32>,
timed_out: bool,
) -> CommandResult {
CommandResult {
success,
stdout: String::new(),
stderr: stderr.to_string(),
timed_out,
exit_code,
}
}
#[test]
fn toolchain_missing_only_on_spawn_failure() {
assert!(toolchain_missing(&cmd_result(
false,
"failed to spawn `rustc`: program not found",
None,
false,
)));
assert!(!toolchain_missing(&cmd_result(
false,
"error: expected `;`",
Some(1),
false,
)));
assert!(!toolchain_missing(&cmd_result(
false,
"timed out after 30s",
None,
true,
)));
assert!(!toolchain_missing(&cmd_result(true, "", Some(0), false)));
}
#[test]
fn rust_failure_distinguishes_parse_from_resolution() {
assert!(rust_failure_is_syntax_error(
"error: expected one of `!` or `::`, found `foo`\n --> src/x.rs:3:5"
));
assert!(rust_failure_is_syntax_error(
"error: this file contains an unclosed delimiter"
));
assert!(rust_failure_is_syntax_error(
"error: unexpected closing delimiter: `}`"
));
assert!(!rust_failure_is_syntax_error(
"error[E0432]: unresolved import `serde`\n --> src/x.rs:1:5"
));
assert!(!rust_failure_is_syntax_error(
"error[E0433]: failed to resolve: use of undeclared crate or module `helpers`"
));
assert!(!rust_failure_is_syntax_error(
"error[E0308]: mismatched types\n expected `u8`, found `&str`"
));
}
#[test]
fn rust_syntax_check_ok_for_valid_file_with_imports() {
let import_only = cmd_result(
false,
"error[E0432]: unresolved import `serde`",
Some(1),
false,
);
assert!(rust_syntax_check_ok(&import_only));
let parse_err = cmd_result(false, "error: expected `;`, found `}`", Some(1), false);
assert!(!rust_syntax_check_ok(&parse_err));
let missing = cmd_result(false, "failed to spawn `rustc`: not found", None, false);
assert!(rust_syntax_check_ok(&missing));
}
#[test]
fn strip_code_blocks_removes_python_fence() {
let input = "```python\ndef greet():\n return 'hi'\n```";
assert_eq!(strip_code_blocks(input), "def greet():\n return 'hi'");
}
#[test]
fn strip_code_blocks_removes_bare_fence() {
let input = "```\nx = 42\n```";
assert_eq!(strip_code_blocks(input), "x = 42");
}
#[test]
fn strip_code_blocks_noop_without_fences() {
let input = "def greet():\n return 'hi'";
assert_eq!(strip_code_blocks(input), input);
}
#[test]
fn strip_code_blocks_handles_trailing_whitespace() {
let input = " ```python\ncode\n``` ";
assert_eq!(strip_code_blocks(input), "code");
}
#[test]
fn strip_code_blocks_extracts_from_text_before_fence() {
let input = "Here's the corrected code:\n```python\ndef greet():\n return 'hi'\n```\n";
assert_eq!(strip_code_blocks(input), "def greet():\n return 'hi'");
}
#[test]
fn strip_code_blocks_multi_block_takes_first() {
let input =
"```bash\n#!/bin/bash\necho hello\n```\n\n# Test cases\n\n```bash\necho test\n```";
assert_eq!(strip_code_blocks(input), "#!/bin/bash\necho hello");
}
#[test]
fn strip_code_blocks_preserves_internal_triple_backticks() {
let input = "import re\nPATTERN = re.compile(r'```(.*?)```', re.DOTALL)\nprint(PATTERN)";
assert_eq!(strip_code_blocks(input), input);
}
#[test]
fn strip_code_blocks_fenced_code_with_internal_backticks() {
let input = "```python\nimport re\nP = re.compile(r'```x```')\n```";
assert_eq!(
strip_code_blocks(input),
"import re\nP = re.compile(r'```x```')"
);
}
#[test]
fn strip_code_blocks_extracts_from_text_before_and_after() {
let input = "Fix applied:\n```python\nx = 42\n```\nThe bug was on line 3.";
assert_eq!(strip_code_blocks(input), "x = 42");
}
#[test]
fn count_failures_from_error_counts_fail_and_error_lines() {
let error = "test_a ... ok\ntest_b ... FAIL\ntest_c ... ERROR\ntest_d ... ok\n";
assert_eq!(count_failures_from_error(error), 2);
}
#[test]
fn count_failures_from_error_returns_one_for_unknown_errors() {
let error = "SyntaxError: invalid syntax\n";
assert_eq!(count_failures_from_error(error), 1);
}
#[test]
fn count_failures_from_error_returns_zero_for_empty() {
assert_eq!(count_failures_from_error(""), 0);
}
#[test]
fn validation_skips_non_code_files() {
let path = Path::new("some-notes.txt");
assert!(validate_code_file(path, &[]).is_none());
}
#[test]
fn validation_skips_unknown_extensions() {
let path = Path::new("data.csv");
assert!(validate_code_file(path, &[]).is_none());
}
#[test]
fn codet_result_to_json_contains_all_fields() {
let result = CodetResult {
syntax_ok: true,
tests_found: true,
tests_passed: 10,
tests_failed: 1,
tests_errors: 0,
fixes_applied: 1,
attempts_made: 2,
fix_summary: "fixed test_get_age".to_string(),
status: CodetStatus::FixedAll,
};
let j = result.to_json();
assert_eq!(j["syntax_ok"], true);
assert_eq!(j["tests_passed"], 10);
assert_eq!(j["tests_failed"], 1);
assert_eq!(j["fixes_applied"], 1);
assert_eq!(j["attempts_made"], 2);
assert!(j["status"].as_str().unwrap().contains("fixed_all"));
}
#[test]
fn parse_single_search_replace_block() {
let input = "<<<<<<< SEARCH\nfoo\n=======\nbar\n>>>>>>> REPLACE";
let patches = parse_search_replace_blocks(input);
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].search, "foo");
assert_eq!(patches[0].replace, "bar");
}
#[test]
fn parse_multiple_blocks() {
let input = "<<<<<<< SEARCH\nfoo\n=======\nbar\n>>>>>>> REPLACE\n\n<<<<<<< SEARCH\nbaz\n=======\nqux\n>>>>>>> REPLACE";
let patches = parse_search_replace_blocks(input);
assert_eq!(patches.len(), 2);
assert_eq!(patches[0].search, "foo");
assert_eq!(patches[1].search, "baz");
}
#[test]
fn parse_tolerates_surrounding_prose() {
let input = "Here is the fix:\n<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE\nDone.";
let patches = parse_search_replace_blocks(input);
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].search, "old");
}
#[test]
fn parse_rejects_missing_closing_mark() {
let input = "<<<<<<< SEARCH\nfoo\n=======\nbar\n"; let patches = parse_search_replace_blocks(input);
assert!(patches.is_empty());
}
#[test]
fn parse_rejects_missing_separator() {
let input = "<<<<<<< SEARCH\nfoo\n>>>>>>> REPLACE"; let patches = parse_search_replace_blocks(input);
assert!(patches.is_empty());
}
#[test]
fn parse_multiline_replacement() {
let input = "<<<<<<< SEARCH\nfoo\n=======\nbar\nbaz\nqux\n>>>>>>> REPLACE";
let patches = parse_search_replace_blocks(input);
assert_eq!(patches.len(), 1);
assert_eq!(patches[0].replace, "bar\nbaz\nqux");
}
#[test]
fn apply_single_patch_replaces_exact_match() {
let content = "def foo():\n return x\n";
let patches = vec![Patch {
search: "return x".to_string(),
replace: "return 42".to_string(),
}];
let result = apply_patches(content, &patches).unwrap();
assert_eq!(result, "def foo():\n return 42\n");
}
#[test]
fn apply_multiple_patches_sequential() {
let content = "a = 1\nb = 2\n";
let patches = vec![
Patch {
search: "a = 1".to_string(),
replace: "a = 10".to_string(),
},
Patch {
search: "b = 2".to_string(),
replace: "b = 20".to_string(),
},
];
let result = apply_patches(content, &patches).unwrap();
assert_eq!(result, "a = 10\nb = 20\n");
}
#[test]
fn apply_fails_when_search_missing() {
let content = "a = 1\n";
let patches = vec![Patch {
search: "b = 2".to_string(),
replace: "c = 3".to_string(),
}];
assert!(apply_patches(content, &patches).is_none());
}
#[test]
fn apply_fails_on_non_unique_search() {
let content = "x = 1\ny = 2\nx = 1\n";
let patches = vec![Patch {
search: "x = 1".to_string(),
replace: "x = 99".to_string(),
}];
assert!(apply_patches(content, &patches).is_none());
}
#[test]
fn apply_fuzzy_match_tolerates_trailing_whitespace() {
let content = "def foo(): \n return x\n";
let patches = vec![Patch {
search: "def foo():\n return x".to_string(),
replace: "def foo():\n return 42".to_string(),
}];
let result = apply_patches(content, &patches).unwrap();
assert!(result.contains("return 42"));
}
#[test]
fn fixes_real_syntax_break_from_md2html() {
let content = "patterns = {\n 'block_code': re.compile(r'\n 'paragraph': re.compile(r'.+'),\n}\n";
let patches = vec![Patch {
search: "'block_code': re.compile(r'".to_string(),
replace: "'block_code': re.compile(r'```'),".to_string(),
}];
let result = apply_patches(content, &patches).unwrap();
assert!(result.contains("```'),"));
}
#[test]
fn apply_fuzzy_match_handles_crlf_without_corruption() {
let content = "fn a() {\r\n let x = 1;\r\n}\r\n";
let patches = vec![Patch {
search: "fn a() {\n let x = 1;\n}".to_string(),
replace: "fn a() {\r\n let y = 2;\r\n}".to_string(),
}];
let result = apply_patches(content, &patches).unwrap();
assert_eq!(result, "fn a() {\r\n let y = 2;\r\n}\r\n");
}
#[test]
fn fuzzy_find_crlf_offsets_stay_on_char_boundary() {
let haystack = "let s = \"héllo\";\r\nTARGET\r\nlet z = 9;\r\n";
let (start, end) = fuzzy_find(haystack, "TARGET").unwrap();
assert!(haystack.is_char_boundary(start));
assert!(haystack.is_char_boundary(end));
assert_eq!(&haystack[start..end], "TARGET");
}
}