Skip to content

Commit 42f5ae9

Browse files
committed
fix: harden review prompts, feedback signals, and security heuristics
1 parent 5bf1ca7 commit 42f5ae9

10 files changed

Lines changed: 718 additions & 305 deletions

File tree

src/commands/misc.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,16 +220,12 @@ pub async fn feedback_command(
220220
let now = chrono::Utc::now().to_rfc3339();
221221
let is_accepted = action == "accept";
222222
for comment in &comments {
223-
let ext = comment
224-
.file_path
225-
.extension()
226-
.and_then(|e| e.to_str())
227-
.map(|e| format!("*.{}", e));
223+
let file_patterns = review::derive_file_patterns(&comment.file_path);
228224
cstore.record_feedback(
229225
&comment.content,
230226
&comment.category.to_string(),
231227
is_accepted,
232-
ext.as_deref(),
228+
file_patterns.first().map(String::as_str),
233229
&now,
234230
);
235231
}
@@ -253,6 +249,8 @@ fn apply_feedback_accept(store: &mut review::FeedbackStore, comments: &[core::Co
253249
let key = review::classify_comment_type(comment).as_str().to_string();
254250
let stats = store.by_comment_type.entry(key).or_default();
255251
stats.accepted = stats.accepted.saturating_add(1);
252+
let file_patterns = review::derive_file_patterns(&comment.file_path);
253+
store.record_feedback_patterns(&comment.category.to_string(), &file_patterns, true);
256254
}
257255
store.suppress.remove(&comment.id);
258256
}
@@ -268,6 +266,8 @@ fn apply_feedback_reject(store: &mut review::FeedbackStore, comments: &[core::Co
268266
let key = review::classify_comment_type(comment).as_str().to_string();
269267
let stats = store.by_comment_type.entry(key).or_default();
270268
stats.rejected = stats.rejected.saturating_add(1);
269+
let file_patterns = review::derive_file_patterns(&comment.file_path);
270+
store.record_feedback_patterns(&comment.category.to_string(), &file_patterns, false);
271271
}
272272
store.accept.remove(&comment.id);
273273
}
@@ -565,5 +565,8 @@ mod tests {
565565
stats.accepted, 1,
566566
"Stats should only count 1 acceptance, not 2 (double-counting bug)"
567567
);
568+
assert_eq!(store.by_category["Bug"].accepted, 1);
569+
assert_eq!(store.by_file_pattern["*.rs"].accepted, 1);
570+
assert_eq!(store.by_category_file_pattern["Bug|*.rs"].accepted, 1);
568571
}
569572
}

src/core/agent_tools.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Result;
22
use async_trait::async_trait;
33
use serde::{Deserialize, Serialize};
44
use serde_json::json;
5+
use std::collections::HashMap;
56
use std::path::PathBuf;
67
use std::sync::Arc;
78

@@ -612,19 +613,34 @@ impl ReviewTool for GitBlameTool {
612613

613614
// git2::Repository is not Send, so we must open it inside spawn_blocking
614615
let result = tokio::task::spawn_blocking(move || -> Result<String> {
615-
let repo = git2::Repository::open(&repo_path)?;
616-
let blame = match repo.blame_file(std::path::Path::new(&file_path_owned), None) {
617-
Ok(b) => b,
618-
Err(e) => return Ok(format!("Error: could not blame file: {}", e)),
619-
};
620-
621-
// Read file to count lines
616+
// Read file first so we can validate and clamp the requested range.
622617
let content = std::fs::read_to_string(repo_path.join(&file_path_owned))?;
623618
let line_count = content.lines().count();
624619

620+
if line_count == 0 {
621+
return Ok("No blame data available for the specified range.".to_string());
622+
}
623+
625624
let start = start_line.unwrap_or(1).max(1);
626625
let end = end_line.unwrap_or(line_count).min(line_count);
626+
if start > end {
627+
return Ok("No blame data available for the specified range.".to_string());
628+
}
629+
630+
let repo = git2::Repository::open(&repo_path)?;
631+
let mut blame_options = git2::BlameOptions::new();
632+
blame_options.min_line(start);
633+
blame_options.max_line(end);
634+
635+
let blame = match repo.blame_file(
636+
std::path::Path::new(&file_path_owned),
637+
Some(&mut blame_options),
638+
) {
639+
Ok(b) => b,
640+
Err(e) => return Ok(format!("Error: could not blame file: {}", e)),
641+
};
627642

643+
let mut commit_message_cache: HashMap<git2::Oid, String> = HashMap::new();
628644
let mut output = String::new();
629645
for line_num in start..=end {
630646
if let Some(hunk) = blame.get_line(line_num) {
@@ -639,14 +655,18 @@ impl ReviewTool for GitBlameTool {
639655
.unwrap_or_else(|| "unknown".to_string());
640656

641657
// Get commit message (first line only)
642-
let message = repo
643-
.find_commit(oid)
644-
.ok()
645-
.and_then(|c| {
646-
c.message()
647-
.map(|m| m.lines().next().unwrap_or("").to_string())
658+
let message = commit_message_cache
659+
.entry(oid)
660+
.or_insert_with(|| {
661+
repo.find_commit(oid)
662+
.ok()
663+
.and_then(|c| {
664+
c.message()
665+
.map(|m| m.lines().next().unwrap_or("").to_string())
666+
})
667+
.unwrap_or_default()
648668
})
649-
.unwrap_or_default();
669+
.clone();
650670

651671
output.push_str(&format!(
652672
"L{}: {} ({}) [{}] {}\n",

0 commit comments

Comments
 (0)