Skip to content

Commit cbada52

Browse files
committed
Add strictness, comment-type controls, and adaptive review filtering
1 parent dc36b5e commit cbada52

File tree

5 files changed

+484
-11
lines changed

5 files changed

+484
-11
lines changed

.diffscope.yml.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ max_tokens: 4000
88
max_context_chars: 20000 # 0 disables context truncation
99
max_diff_chars: 40000 # 0 disables diff truncation
1010
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
11+
strictness: 2 # 1 = high-signal only, 2 = balanced, 3 = deep scan
12+
comment_types: # logic | syntax | style | informational
13+
- logic
14+
- syntax
15+
- style
16+
- informational
1117
review_profile: balanced # balanced | chill | assertive
1218
review_instructions: |
1319
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
@@ -28,6 +34,17 @@ symbol_index_max_bytes: 200000
2834
symbol_index_max_locations: 5
2935
feedback_path: ".diffscope.feedback.json"
3036

37+
# Optional Greptile-like scoped context bundles.
38+
# notes become additional reviewer instructions, files are loaded as reference context.
39+
custom_context:
40+
- scope: "src/api/**"
41+
notes:
42+
- "Auth endpoints must enforce tenant isolation and rate limits."
43+
- "Prefer idempotent handlers for retries."
44+
files:
45+
- "docs/security/*.md"
46+
- "src/config/**/*.yml"
47+
3148
# API configuration (optional - can use environment variables)
3249
# api_key: your-api-key-here
3350
# base_url: https://api.openai.com/v1

FEATURES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,10 @@ This enhanced system provides professional-grade code review capabilities while
103103
- Regex and LSP-backed symbol indexing with auto-detection when `symbol_index_lsp_command` is omitted.
104104
- LSP preflight command: `diffscope lsp-check` validates binaries, language IDs, and unmapped extensions.
105105
- Ready-to-copy LSP configs in `examples/lsp/` and setup guide in `docs/lsp.md`.
106+
107+
## Signal And Context Controls
108+
109+
- `strictness` levels (1-3) to control review depth and noise.
110+
- `comment_types` filtering (`logic`, `syntax`, `style`, `informational`) to focus output.
111+
- Scoped `custom_context` entries to inject path-specific notes and context files.
112+
- Adaptive suppression of repeated low-value comment types based on accepted/rejected feedback history.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ A composable code review engine for automated diff analysis.
1515
- **CI/CD Ready**: GitHub Action, GitLab CI, and Docker support
1616
- **Smart Review**: Enhanced analysis with confidence scoring, fix effort estimation, and executive summaries
1717
- **Path-Based Configuration**: Customize review behavior for different parts of your codebase
18+
- **Signal Controls**: Tune strictness and comment types (`logic`, `syntax`, `style`, `informational`)
19+
- **Adaptive Learning**: Suppress low-value recurring feedback based on accepted/rejected review history
20+
- **Scoped Custom Context**: Attach rules and reference files to path scopes for higher-precision reviews
1821
- **Changelog Generation**: Generate changelogs and release notes from git history
1922
- **Interactive Commands**: Respond to PR comments with @diffscope commands
2023

@@ -193,6 +196,12 @@ max_tokens: 4000
193196
max_context_chars: 20000 # 0 disables context truncation
194197
max_diff_chars: 40000 # 0 disables diff truncation
195198
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
199+
strictness: 2 # 1 = high-signal only, 2 = balanced, 3 = deep scan
200+
comment_types:
201+
- logic
202+
- syntax
203+
- style
204+
- informational
196205
review_profile: balanced # balanced | chill | assertive
197206
review_instructions: |
198207
Prioritize security and correctness issues. Avoid stylistic comments unless they impact maintainability.
@@ -210,6 +219,14 @@ feedback_path: ".diffscope.feedback.json"
210219
system_prompt: "Focus on security vulnerabilities, performance issues, and best practices"
211220
openai_use_responses: true # Use OpenAI Responses API (recommended) instead of chat completions
212221
222+
custom_context:
223+
- scope: "src/api/**"
224+
notes:
225+
- "Auth flows must enforce tenant boundaries and rate limits."
226+
files:
227+
- "docs/security/*.md"
228+
- "src/config/**/*.yml"
229+
213230
# Built-in plugins (enabled by default)
214231
plugins:
215232
eslint: true # JavaScript/TypeScript linting

src/config.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ pub struct Config {
2323
#[serde(default = "default_min_confidence")]
2424
pub min_confidence: f32,
2525

26+
#[serde(default = "default_strictness")]
27+
pub strictness: u8,
28+
29+
#[serde(default = "default_comment_types")]
30+
pub comment_types: Vec<String>,
31+
2632
#[serde(default)]
2733
pub review_profile: Option<String>,
2834

@@ -74,6 +80,9 @@ pub struct Config {
7480

7581
#[serde(default)]
7682
pub paths: HashMap<String, PathConfig>,
83+
84+
#[serde(default)]
85+
pub custom_context: Vec<CustomContextConfig>,
7786
}
7887

7988
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -95,6 +104,18 @@ pub struct PathConfig {
95104
pub severity_overrides: HashMap<String, String>,
96105
}
97106

107+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
108+
pub struct CustomContextConfig {
109+
#[serde(default)]
110+
pub scope: Option<String>,
111+
112+
#[serde(default)]
113+
pub notes: Vec<String>,
114+
115+
#[serde(default)]
116+
pub files: Vec<String>,
117+
}
118+
98119
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
99120
pub struct PluginConfig {
100121
#[serde(default = "default_true")]
@@ -116,6 +137,8 @@ impl Default for Config {
116137
max_context_chars: default_max_context_chars(),
117138
max_diff_chars: default_max_diff_chars(),
118139
min_confidence: default_min_confidence(),
140+
strictness: default_strictness(),
141+
comment_types: default_comment_types(),
119142
review_profile: None,
120143
review_instructions: None,
121144
smart_review_summary: true,
@@ -135,6 +158,7 @@ impl Default for Config {
135158
plugins: PluginConfig::default(),
136159
exclude_patterns: Vec::new(),
137160
paths: HashMap::new(),
161+
custom_context: Vec::new(),
138162
}
139163
}
140164
}
@@ -225,6 +249,13 @@ impl Config {
225249
} else if !(0.0..=1.0).contains(&self.min_confidence) {
226250
self.min_confidence = self.min_confidence.clamp(0.0, 1.0);
227251
}
252+
if self.strictness == 0 {
253+
self.strictness = default_strictness();
254+
} else if self.strictness > 3 {
255+
self.strictness = 3;
256+
}
257+
258+
self.comment_types = normalize_comment_types(&self.comment_types);
228259

229260
if let Some(profile) = &self.review_profile {
230261
let normalized = profile.trim().to_lowercase();
@@ -242,6 +273,37 @@ impl Config {
242273
self.review_instructions = None;
243274
}
244275
}
276+
277+
let mut normalized_custom_context = Vec::new();
278+
for mut entry in std::mem::take(&mut self.custom_context) {
279+
entry.scope = entry.scope.and_then(|scope| {
280+
let trimmed = scope.trim().to_string();
281+
if trimmed.is_empty() {
282+
None
283+
} else {
284+
Some(trimmed)
285+
}
286+
});
287+
288+
entry.notes = entry
289+
.notes
290+
.into_iter()
291+
.map(|note| note.trim().to_string())
292+
.filter(|note| !note.is_empty())
293+
.collect();
294+
entry.files = entry
295+
.files
296+
.into_iter()
297+
.map(|file| file.trim().to_string())
298+
.filter(|file| !file.is_empty())
299+
.collect();
300+
301+
if entry.notes.is_empty() && entry.files.is_empty() {
302+
continue;
303+
}
304+
normalized_custom_context.push(entry);
305+
}
306+
self.custom_context = normalized_custom_context;
245307
}
246308

247309
pub fn get_path_config(&self, file_path: &Path) -> Option<&PathConfig> {
@@ -284,6 +346,26 @@ impl Config {
284346
false
285347
}
286348

349+
pub fn matching_custom_context(&self, file_path: &Path) -> Vec<&CustomContextConfig> {
350+
let file_path_str = file_path.to_string_lossy();
351+
self.custom_context
352+
.iter()
353+
.filter(|entry| match entry.scope.as_deref() {
354+
Some(scope) => self.path_matches(&file_path_str, scope),
355+
None => true,
356+
})
357+
.collect()
358+
}
359+
360+
pub fn effective_min_confidence(&self) -> f32 {
361+
let strictness_floor = match self.strictness {
362+
1 => 0.85,
363+
2 => 0.65,
364+
_ => 0.45,
365+
};
366+
self.min_confidence.max(strictness_floor).clamp(0.0, 1.0)
367+
}
368+
287369
fn path_matches(&self, path: &str, pattern: &str) -> bool {
288370
// Simple glob matching
289371
if pattern.contains('*') {
@@ -310,6 +392,7 @@ mod tests {
310392
config.temperature = 5.0;
311393
config.max_tokens = 0;
312394
config.min_confidence = 2.0;
395+
config.strictness = 0;
313396
config.review_profile = Some("ASSERTIVE".to_string());
314397

315398
config.normalize();
@@ -318,8 +401,24 @@ mod tests {
318401
assert_eq!(config.temperature, default_temperature());
319402
assert_eq!(config.max_tokens, default_max_tokens());
320403
assert_eq!(config.min_confidence, 1.0);
404+
assert_eq!(config.strictness, default_strictness());
321405
assert_eq!(config.review_profile.as_deref(), Some("assertive"));
322406
}
407+
408+
#[test]
409+
fn normalize_comment_types_filters_unknown_values() {
410+
let mut config = Config::default();
411+
config.comment_types = vec![
412+
" LOGIC ".to_string(),
413+
"style".to_string(),
414+
"unknown".to_string(),
415+
"STYLE".to_string(),
416+
];
417+
418+
config.normalize();
419+
420+
assert_eq!(config.comment_types, vec!["logic", "style"]);
421+
}
323422
}
324423

325424
fn default_model() -> String {
@@ -346,6 +445,19 @@ fn default_min_confidence() -> f32 {
346445
0.0
347446
}
348447

448+
fn default_strictness() -> u8 {
449+
2
450+
}
451+
452+
fn default_comment_types() -> Vec<String> {
453+
vec![
454+
"logic".to_string(),
455+
"syntax".to_string(),
456+
"style".to_string(),
457+
"informational".to_string(),
458+
]
459+
}
460+
349461
fn default_symbol_index_max_files() -> usize {
350462
500
351463
}
@@ -375,3 +487,29 @@ fn default_feedback_path() -> PathBuf {
375487
fn default_true() -> bool {
376488
true
377489
}
490+
491+
fn normalize_comment_types(values: &[String]) -> Vec<String> {
492+
if values.is_empty() {
493+
return default_comment_types();
494+
}
495+
496+
let mut normalized = Vec::new();
497+
for value in values {
498+
let value = value.trim().to_lowercase();
499+
if !matches!(
500+
value.as_str(),
501+
"logic" | "syntax" | "style" | "informational"
502+
) {
503+
continue;
504+
}
505+
if !normalized.contains(&value) {
506+
normalized.push(value);
507+
}
508+
}
509+
510+
if normalized.is_empty() {
511+
default_comment_types()
512+
} else {
513+
normalized
514+
}
515+
}

0 commit comments

Comments
 (0)