use std::sync::LazyLock;
use crate::engine::diff_parser::{DiffLineType, parse_diff};
pub const MAX_CONTEXT_LINES: usize = 120;
static RE_BRANCHING: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"(\bmatch\b|\bif\b|\belse\b|=>|\breturn\b|\.\?|\?\s*;)").unwrap()
});
#[derive(Debug, Clone)]
pub struct EnclosingSnippet {
pub file: String,
pub start_line: usize,
pub lines: usize,
}
pub fn extract_enclosing_snippets(diff: &str, root: &std::path::Path) -> Vec<EnclosingSnippet> {
let mut out = Vec::new();
for chunk in parse_diff(diff) {
if chunk.is_binary || chunk.is_deleted || chunk.is_new {
continue;
}
let Some(path) = &chunk.new_path else {
continue;
};
for hunk in &chunk.chunks {
let touches_branching = hunk
.lines
.iter()
.any(|l| l.line_type != DiffLineType::Remove && RE_BRANCHING.is_match(&l.content));
if !touches_branching {
continue;
}
let Ok(content) = std::fs::read_to_string(root.join(path)) else {
continue;
};
let lines: Vec<&str> = content.lines().collect();
let hit = hunk
.lines
.iter()
.filter(|l| l.line_type != DiffLineType::Remove)
.find_map(|l| l.new_line_no)
.unwrap_or(hunk.new_start);
if let Some((start_idx, end_idx)) = enclosing_block_span(&lines, hit as usize) {
out.push(EnclosingSnippet {
file: path.clone(),
start_line: start_idx + 1,
lines: end_idx - start_idx + 1,
});
}
}
}
out
}
pub fn render_for_prompt(
snippets: &[EnclosingSnippet],
file_lines: impl Fn(&str) -> Option<Vec<String>>,
) -> String {
if snippets.is_empty() {
return String::new();
}
let mut s = String::from(
"Surrounding code from the post-image — reference only, NOT part of the diff; \
verify branch structure here before making reachability claims:\n",
);
for sn in snippets {
let Some(all) = file_lines(&sn.file) else {
continue;
};
let start = sn.start_line.saturating_sub(1);
let end = (start + sn.lines).min(all.len());
s.push_str(&format!(
"=== {} (lines {}-{}) ===\n{}\n\n",
sn.file,
start + 1,
end,
clamp_context_text(&all[start..end].join("\n"))
));
}
s
}
fn enclosing_block_span(lines: &[&str], hit_line: usize) -> Option<(usize, usize)> {
if lines.is_empty() || hit_line == 0 || hit_line > lines.len() {
return None;
}
const DEF_HINTS: [&str; 4] = ["fn ", "func ", "def ", "function"];
let mut opens: Vec<(usize, bool)> = Vec::new();
for (i, line) in lines.iter().enumerate() {
for c in line.chars() {
match c {
'{' => opens.push((i + 1, DEF_HINTS.iter().any(|k| line.contains(k)))),
'}' => {
opens.pop();
}
_ => {}
}
}
if i + 1 >= hit_line {
break;
}
}
let open_line = opens
.iter()
.rev()
.find(|(_, looks_def)| *looks_def)
.or_else(|| opens.last())
.map(|(l, _)| *l)?;
let mut depth = 0i64;
for (i, line) in lines.iter().enumerate().skip(open_line - 1) {
for c in line.chars() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
}
if depth <= 0 {
return Some((open_line - 1, i));
}
}
Some((open_line - 1, lines.len() - 1))
}
pub(crate) fn clamp_context_text(text: &str) -> String {
let count = text.lines().count();
if count <= MAX_CONTEXT_LINES {
return text.to_string();
}
let head = MAX_CONTEXT_LINES * 2 / 3;
let tail = MAX_CONTEXT_LINES - head - 1; let mut kept: Vec<&str> = text.lines().take(head).collect();
kept.push("…");
kept.extend(text.lines().skip(count - tail));
kept.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = r#"handlers.rs"#;
const FIXTURE_BODY: &str = r#"use crate::store::Store;
fn remember(store: &mut Store, text: &str) -> usize { store.remember(text) }
fn remember_with_contradiction(store: &mut Store, text: &str) -> (usize, bool) {
let id = store.remember(text);
(id, true)
}
fn handle_request(store: &mut Store, detect_contradiction: bool) -> usize {
let author_type = "user";
if !author_type.is_empty() { /* validated up front */ }
let result = if detect_contradiction {
remember_with_contradiction(store, "note").map(|(id, _)| id)
} else {
Ok(remember(store, "note"))
};
match result {
Ok(id) => {
store.set_author_type(id);
id
}
Err(_) => 0,
}
}
"#;
fn write_fixture(dir: &std::path::Path) {
std::fs::write(dir.join(FIXTURE), FIXTURE_BODY).unwrap();
}
fn fixture_diff() -> String {
let body_line = |n: usize| FIXTURE_BODY.lines().nth(n - 1).unwrap_or("");
let mut d = String::from("--- a/handlers.rs\n+++ b/handlers.rs\n");
d.push_str("@@ -19,6 +19,7 @@\n");
for n in 19..=21 {
d.push_str(&format!(" {}\n", body_line(n)));
}
d.push_str("+ store.set_author_type(id);\n");
for n in 22..=24 {
d.push_str(&format!(" {}\n", body_line(n)));
}
d
}
#[test]
fn acceptance_shared_match_arm_context_included() {
let tmp = tempfile::tempdir().unwrap();
write_fixture(tmp.path());
let snippets = extract_enclosing_snippets(&fixture_diff(), tmp.path());
assert!(!snippets.is_empty(), "branching hunk must get context");
assert_eq!(snippets[0].file, FIXTURE);
let rendered = render_for_prompt(&snippets, |f| {
std::fs::read_to_string(tmp.path().join(f))
.map(|c| c.lines().map(String::from).collect())
.ok()
});
assert!(
rendered.contains("remember_with_contradiction"),
"producer 1 must appear in surrounding code"
);
assert!(
rendered.contains("Ok(remember("),
"producer 2 must appear in surrounding code"
);
assert!(
rendered.contains("set_author_type"),
"the changed arm itself"
);
assert!(rendered.starts_with("Surrounding code"));
}
#[test]
fn no_context_without_branching_lines() {
let tmp = tempfile::tempdir().unwrap();
write_fixture(tmp.path());
let diff = "--- a/handlers.rs\n+++ b/handlers.rs\n@@ -2,3 +2,4 @@\n use crate::store::Store;\n+let total = 1 + 2;\n fn remember";
let snippets = extract_enclosing_snippets(diff, tmp.path());
assert!(snippets.is_empty(), "no branching → no injection");
}
#[test]
fn new_files_are_skipped() {
let tmp = tempfile::tempdir().unwrap();
let diff = "--- /dev/null\n+++ b/newthing.rs\n@@ -0,0 +1,2 @@\n+fn x(a: u8) {\n+ match a { _ => {} }\n}";
let snippets = extract_enclosing_snippets(diff, tmp.path());
assert!(snippets.is_empty(), "new file content is already the diff");
}
#[test]
fn long_functions_are_clamped() {
let big: String = std::iter::once("fn huge() {".to_string())
.chain((0..400).map(|i| format!(" let v{i} = {i};")))
.chain(std::iter::once("}".to_string()))
.collect::<Vec<_>>()
.join("\n");
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("big.rs"), &big).unwrap();
let diff = format!(
"--- a/big.rs\n+++ b/big.rs\n@@ -398,4 +398,5 @@\n{}\n+ if v399 {{}}\n}}",
big.lines().nth(397).unwrap()
);
let snippets = extract_enclosing_snippets(&diff, tmp.path());
assert!(!snippets.is_empty());
let rendered = render_for_prompt(&snippets, |f| {
std::fs::read_to_string(tmp.path().join(f))
.map(|c| c.lines().map(String::from).collect())
.ok()
});
let emitted = rendered.lines().count();
assert!(
emitted <= MAX_CONTEXT_LINES + 6,
"bounded emission expected, got {emitted} lines"
);
assert!(rendered.contains('…'), "clamp marker present");
}
#[test]
fn guardrail_rule_is_part_of_prompt() {
let prompt = super::super::llm::build_review_prompt("d", &[], &[], None, None);
assert!(
prompt.contains(crate::engine::llm::CONTROL_FLOW_GUARDRAIL),
"guardrail text must always ship in review prompts"
);
}
}