Skip to content

Commit dbe8113

Browse files
committed
Add review guidance profiles and confidence filtering
1 parent aabcdae commit dbe8113

File tree

5 files changed

+158
-3
lines changed

5 files changed

+158
-3
lines changed

.diffscope.yml.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ temperature: 0.2
77
max_tokens: 4000
88
max_context_chars: 20000 # 0 disables context truncation
99
max_diff_chars: 40000 # 0 disables diff truncation
10+
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
11+
review_profile: balanced # balanced | chill | assertive
12+
review_instructions: |
13+
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
1014

1115
# API configuration (optional - can use environment variables)
1216
# api_key: your-api-key-here
@@ -30,6 +34,8 @@ paths:
3034
- security
3135
- validation
3236
- authentication
37+
review_instructions: |
38+
Treat auth and input validation as critical. Flag missing rate limits and unsafe defaults.
3339
severity_overrides:
3440
security: error # Elevate all security issues to errors
3541
system_prompt: |

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,12 @@ Create a `.diffscope.yml` file in your repository:
190190
model: gpt-4o
191191
temperature: 0.2
192192
max_tokens: 4000
193-
max_context_chars: 20000
194-
max_diff_chars: 40000
195193
max_context_chars: 20000 # 0 disables context truncation
196194
max_diff_chars: 40000 # 0 disables diff truncation
195+
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
196+
review_profile: balanced # balanced | chill | assertive
197+
review_instructions: |
198+
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
197199
system_prompt: "Focus on security vulnerabilities, performance issues, and best practices"
198200
openai_use_responses: true # Use OpenAI Responses API (recommended) instead of chat completions
199201
@@ -403,6 +405,8 @@ For a large Python/FastAPI application at a company like Acme Inc:
403405
model: "claude-3-5-sonnet-20241022"
404406
temperature: 0.1 # Low for consistent reviews
405407
max_tokens: 4000
408+
min_confidence: 0.2
409+
review_profile: assertive
406410

407411
system_prompt: |
408412
You are reviewing Python code for a production FastAPI application.
@@ -560,6 +564,8 @@ paths:
560564
- "**/*.generated.*"
561565
extra_context:
562566
- "src/auth/**"
567+
review_instructions: |
568+
Prioritize auth, validation, and sensitive data handling.
563569
system_prompt: |
564570
Focus on SQL injection, auth bypass, and input validation
565571
severity_overrides:

src/config.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ pub struct Config {
2020
#[serde(default = "default_max_diff_chars")]
2121
pub max_diff_chars: usize,
2222

23+
#[serde(default = "default_min_confidence")]
24+
pub min_confidence: f32,
25+
26+
#[serde(default)]
27+
pub review_profile: Option<String>,
28+
29+
#[serde(default)]
30+
pub review_instructions: Option<String>,
31+
2332
pub system_prompt: Option<String>,
2433
pub api_key: Option<String>,
2534
pub base_url: Option<String>,
@@ -50,6 +59,8 @@ pub struct PathConfig {
5059

5160
pub system_prompt: Option<String>,
5261

62+
pub review_instructions: Option<String>,
63+
5364
#[serde(default)]
5465
pub severity_overrides: HashMap<String, String>,
5566
}
@@ -73,6 +84,7 @@ impl Default for PathConfig {
7384
ignore_patterns: Vec::new(),
7485
extra_context: Vec::new(),
7586
system_prompt: None,
87+
review_instructions: None,
7688
severity_overrides: HashMap::new(),
7789
}
7890
}
@@ -86,6 +98,9 @@ impl Default for Config {
8698
max_tokens: default_max_tokens(),
8799
max_context_chars: default_max_context_chars(),
88100
max_diff_chars: default_max_diff_chars(),
101+
min_confidence: default_min_confidence(),
102+
review_profile: None,
103+
review_instructions: None,
89104
system_prompt: None,
90105
api_key: None,
91106
base_url: None,
@@ -150,6 +165,29 @@ impl Config {
150165
if self.max_tokens == 0 {
151166
self.max_tokens = default_max_tokens();
152167
}
168+
169+
if !self.min_confidence.is_finite() {
170+
self.min_confidence = default_min_confidence();
171+
} else if !(0.0..=1.0).contains(&self.min_confidence) {
172+
self.min_confidence = self.min_confidence.clamp(0.0, 1.0);
173+
}
174+
175+
if let Some(profile) = &self.review_profile {
176+
let normalized = profile.trim().to_lowercase();
177+
self.review_profile = if normalized.is_empty() {
178+
None
179+
} else if matches!(normalized.as_str(), "balanced" | "chill" | "assertive") {
180+
Some(normalized)
181+
} else {
182+
None
183+
};
184+
}
185+
186+
if let Some(instructions) = &self.review_instructions {
187+
if instructions.trim().is_empty() {
188+
self.review_instructions = None;
189+
}
190+
}
153191
}
154192

155193
pub fn get_path_config(&self, file_path: &PathBuf) -> Option<&PathConfig> {
@@ -217,12 +255,16 @@ mod tests {
217255
config.model = " ".to_string();
218256
config.temperature = 5.0;
219257
config.max_tokens = 0;
258+
config.min_confidence = 2.0;
259+
config.review_profile = Some("ASSERTIVE".to_string());
220260

221261
config.normalize();
222262

223263
assert_eq!(config.model, default_model());
224264
assert_eq!(config.temperature, default_temperature());
225265
assert_eq!(config.max_tokens, default_max_tokens());
266+
assert_eq!(config.min_confidence, 1.0);
267+
assert_eq!(config.review_profile.as_deref(), Some("assertive"));
226268
}
227269
}
228270

@@ -246,6 +288,10 @@ fn default_max_diff_chars() -> usize {
246288
40000
247289
}
248290

291+
fn default_min_confidence() -> f32 {
292+
0.0
293+
}
294+
249295
fn default_true() -> bool {
250296
true
251297
}

src/core/smart_review_prompt.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ impl SmartReviewPromptBuilder {
99
context_chunks: &[LLMContextChunk],
1010
max_context_chars: usize,
1111
max_diff_chars: usize,
12+
system_prompt_suffix: Option<&str>,
1213
) -> Result<(String, String)> {
13-
let system_prompt = Self::build_smart_review_system_prompt();
14+
let mut system_prompt = Self::build_smart_review_system_prompt();
15+
if let Some(suffix) = system_prompt_suffix {
16+
let trimmed = suffix.trim();
17+
if !trimmed.is_empty() {
18+
system_prompt.push_str("\n\n");
19+
system_prompt.push_str(trimmed);
20+
}
21+
}
1422
let user_prompt = Self::build_smart_review_user_prompt(
1523
diff,
1624
context_chunks,

src/main.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ async fn review_command(
346346
}
347347
}
348348

349+
if let Some(guidance) = build_review_guidance(&config, path_config) {
350+
local_prompt_config.system_prompt.push_str("\n\n");
351+
local_prompt_config.system_prompt.push_str(&guidance);
352+
}
353+
349354
let local_prompt_builder = core::PromptBuilder::new(local_prompt_config);
350355
let (system_prompt, user_prompt) =
351356
local_prompt_builder.build_prompt(&diff, &context_chunks)?;
@@ -389,6 +394,7 @@ async fn review_command(
389394
let processed_comments = plugin_manager
390395
.run_post_processors(all_comments, &repo_path_str)
391396
.await?;
397+
let processed_comments = apply_confidence_threshold(processed_comments, config.min_confidence);
392398

393399
let effective_format = if patch { OutputFormat::Patch } else { format };
394400
output_comments(&processed_comments, output_path, effective_format).await?;
@@ -866,6 +872,10 @@ async fn review_diff_content_raw(
866872
local_prompt_config.system_prompt = prompt.clone();
867873
}
868874
}
875+
if let Some(guidance) = build_review_guidance(&config, path_config) {
876+
local_prompt_config.system_prompt.push_str("\n\n");
877+
local_prompt_config.system_prompt.push_str(&guidance);
878+
}
869879
let local_prompt_builder = core::PromptBuilder::new(local_prompt_config);
870880
let (system_prompt, user_prompt) =
871881
local_prompt_builder.build_prompt(&diff, &context_chunks)?;
@@ -910,6 +920,7 @@ async fn review_diff_content_raw(
910920
let processed_comments = plugin_manager
911921
.run_post_processors(all_comments, &repo_path_str)
912922
.await?;
923+
let processed_comments = apply_confidence_threshold(processed_comments, config.min_confidence);
913924

914925
Ok(processed_comments)
915926
}
@@ -1292,12 +1303,14 @@ async fn smart_review_command(
12921303
context_chunks.extend(definition_chunks);
12931304
}
12941305

1306+
let guidance = build_review_guidance(&config, path_config);
12951307
let (system_prompt, user_prompt) =
12961308
core::SmartReviewPromptBuilder::build_enhanced_review_prompt(
12971309
&diff,
12981310
&context_chunks,
12991311
config.max_context_chars,
13001312
config.max_diff_chars,
1313+
guidance.as_deref(),
13011314
)?;
13021315

13031316
let request = adapters::llm::LLMRequest {
@@ -1340,6 +1353,7 @@ async fn smart_review_command(
13401353
let processed_comments = plugin_manager
13411354
.run_post_processors(all_comments, &repo_path_str)
13421355
.await?;
1356+
let processed_comments = apply_confidence_threshold(processed_comments, config.min_confidence);
13431357

13441358
// Generate summary and output results
13451359
let summary = core::CommentSynthesizer::generate_summary(&processed_comments);
@@ -1825,6 +1839,81 @@ fn filter_comments_for_diff(
18251839
filtered
18261840
}
18271841

1842+
fn build_review_guidance(
1843+
config: &config::Config,
1844+
path_config: Option<&config::PathConfig>,
1845+
) -> Option<String> {
1846+
let mut sections = Vec::new();
1847+
1848+
if let Some(profile) = config.review_profile.as_deref() {
1849+
let guidance = match profile {
1850+
"chill" => Some(
1851+
"Be conservative and only surface high-confidence, high-impact issues. Avoid nitpicks and redundant comments.",
1852+
),
1853+
"assertive" => Some(
1854+
"Be thorough and proactive. Surface edge cases, latent risks, and maintainability concerns even if they are subtle.",
1855+
),
1856+
_ => None,
1857+
};
1858+
if let Some(text) = guidance {
1859+
sections.push(format!("Review profile ({}): {}", profile, text));
1860+
}
1861+
}
1862+
1863+
if let Some(instructions) = config.review_instructions.as_deref() {
1864+
let trimmed = instructions.trim();
1865+
if !trimmed.is_empty() {
1866+
sections.push(format!("Global review instructions:\n{}", trimmed));
1867+
}
1868+
}
1869+
1870+
if let Some(pc) = path_config {
1871+
if let Some(instructions) = pc.review_instructions.as_deref() {
1872+
let trimmed = instructions.trim();
1873+
if !trimmed.is_empty() {
1874+
sections.push(format!("Path-specific instructions:\n{}", trimmed));
1875+
}
1876+
}
1877+
}
1878+
1879+
if sections.is_empty() {
1880+
None
1881+
} else {
1882+
Some(format!(
1883+
"Additional review guidance:\n{}",
1884+
sections.join("\n\n")
1885+
))
1886+
}
1887+
}
1888+
1889+
fn apply_confidence_threshold(
1890+
comments: Vec<core::Comment>,
1891+
min_confidence: f32,
1892+
) -> Vec<core::Comment> {
1893+
if min_confidence <= 0.0 {
1894+
return comments;
1895+
}
1896+
1897+
let total = comments.len();
1898+
let mut kept = Vec::with_capacity(total);
1899+
1900+
for comment in comments {
1901+
if comment.confidence >= min_confidence {
1902+
kept.push(comment);
1903+
}
1904+
}
1905+
1906+
if kept.len() != total {
1907+
let dropped = total.saturating_sub(kept.len());
1908+
info!(
1909+
"Dropped {} comment(s) below confidence threshold {}",
1910+
dropped, min_confidence
1911+
);
1912+
}
1913+
1914+
kept
1915+
}
1916+
18281917
fn is_line_in_diff(diff: &core::UnifiedDiff, line_number: usize) -> bool {
18291918
if line_number == 0 {
18301919
return false;

0 commit comments

Comments
 (0)