Skip to content

Commit b85b4c2

Browse files
committed
Deepen Greptile-style review: rules, ranking, eval, and PR posting
1 parent 90973e5 commit b85b4c2

File tree

8 files changed

+1437
-57
lines changed

8 files changed

+1437
-57
lines changed

.diffscope.yml.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ 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+
context_max_chunks: 24 # Max context chunks sent to the model per file
11+
context_budget_chars: 24000 # Max chars across ranked context chunks per file
1012
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
1113
strictness: 2 # 1 = high-signal only, 2 = balanced, 3 = deep scan
1214
comment_types: # logic | syntax | style | informational
@@ -56,6 +58,17 @@ pattern_repositories:
5658
- "examples/**/*.yml"
5759
max_files: 8
5860
max_lines: 200
61+
rule_patterns:
62+
- "policy/**/*.yml"
63+
- "policy/**/*.json"
64+
max_rules: 200
65+
66+
# Rule files to load directly from this repository.
67+
# If omitted, diffscope auto-discovers .diffscope-rules.{yml,yaml,json} and rules/**/*.y{a,}ml/json.
68+
rules_files:
69+
- ".diffscope-rules.yml"
70+
- "rules/**/*.yml"
71+
max_active_rules: 30
5972

6073
# API configuration (optional - can use environment variables)
6174
# api_key: your-api-key-here

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ diffscope review
8484

8585
# Get enhanced analysis with smart review
8686
git diff | diffscope smart-review
87+
88+
# Evaluate reviewer quality against fixtures
89+
diffscope eval --fixtures eval/fixtures --output eval-report.json
8790
```
8891

8992
### Git Integration
@@ -116,6 +119,27 @@ diffscope pr --number 123
116119
diffscope pr --post-comments
117120
```
118121

122+
`--post-comments` now attempts inline file/line comments first, falls back to PR-level comments when GitHub rejects an anchor, and upserts a sticky DiffScope summary comment on the PR.
123+
124+
### Evaluation Fixtures
125+
```yaml
126+
name: auth guard regression
127+
repo_path: ../../
128+
diff_file: ./auth.patch
129+
expect:
130+
must_find:
131+
- file: src/api/auth.rs
132+
line: 42
133+
contains: missing auth check
134+
severity: error
135+
category: security
136+
rule_id: sec.auth.guard
137+
must_not_find:
138+
- contains: style
139+
min_total: 1
140+
max_total: 8
141+
```
142+
119143
### Smart Review (Enhanced Analysis)
120144
```bash
121145
# Get professional-grade analysis with confidence scoring
@@ -200,6 +224,8 @@ temperature: 0.2
200224
max_tokens: 4000
201225
max_context_chars: 20000 # 0 disables context truncation
202226
max_diff_chars: 40000 # 0 disables diff truncation
227+
context_max_chunks: 24 # Max context chunks sent to the model per file
228+
context_budget_chars: 24000 # Hard cap for ranked context payload per file
203229
min_confidence: 0.0 # Drop comments below this confidence (0.0-1.0)
204230
strictness: 2 # 1 = high-signal only, 2 = balanced, 3 = deep scan
205231
comment_types:
@@ -242,6 +268,16 @@ pattern_repositories:
242268
- "examples/**/*.yml"
243269
max_files: 8
244270
max_lines: 200
271+
rule_patterns:
272+
- "policy/**/*.yml"
273+
- "policy/**/*.json"
274+
max_rules: 200
275+
276+
# Repository-level rule files (YAML/JSON)
277+
rules_files:
278+
- ".diffscope-rules.yml"
279+
- "rules/**/*.yml"
280+
max_active_rules: 30
245281
246282
# Built-in plugins (enabled by default)
247283
plugins:

src/config.rs

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

23+
#[serde(default = "default_context_max_chunks")]
24+
pub context_max_chunks: usize,
25+
26+
#[serde(default = "default_context_budget_chars")]
27+
pub context_budget_chars: usize,
28+
2329
#[serde(default = "default_min_confidence")]
2430
pub min_confidence: f32,
2531

@@ -92,6 +98,12 @@ pub struct Config {
9298

9399
#[serde(default)]
94100
pub pattern_repositories: Vec<PatternRepositoryConfig>,
101+
102+
#[serde(default)]
103+
pub rules_files: Vec<String>,
104+
105+
#[serde(default = "default_max_active_rules")]
106+
pub max_active_rules: usize,
95107
}
96108

97109
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -140,6 +152,12 @@ pub struct PatternRepositoryConfig {
140152

141153
#[serde(default = "default_pattern_repo_max_lines")]
142154
pub max_lines: usize,
155+
156+
#[serde(default)]
157+
pub rule_patterns: Vec<String>,
158+
159+
#[serde(default = "default_pattern_repo_max_rules")]
160+
pub max_rules: usize,
143161
}
144162

145163
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -162,6 +180,8 @@ impl Default for Config {
162180
max_tokens: default_max_tokens(),
163181
max_context_chars: default_max_context_chars(),
164182
max_diff_chars: default_max_diff_chars(),
183+
context_max_chunks: default_context_max_chunks(),
184+
context_budget_chars: default_context_budget_chars(),
165185
min_confidence: default_min_confidence(),
166186
strictness: default_strictness(),
167187
comment_types: default_comment_types(),
@@ -188,6 +208,8 @@ impl Default for Config {
188208
paths: HashMap::new(),
189209
custom_context: Vec::new(),
190210
pattern_repositories: Vec::new(),
211+
rules_files: Vec::new(),
212+
max_active_rules: default_max_active_rules(),
191213
}
192214
}
193215
}
@@ -245,6 +267,12 @@ impl Config {
245267
if self.max_tokens == 0 {
246268
self.max_tokens = default_max_tokens();
247269
}
270+
if self.context_max_chunks == 0 {
271+
self.context_max_chunks = default_context_max_chunks();
272+
}
273+
if self.context_budget_chars == 0 {
274+
self.context_budget_chars = default_context_budget_chars();
275+
}
248276

249277
if self.symbol_index_max_files == 0 {
250278
self.symbol_index_max_files = default_symbol_index_max_files();
@@ -369,10 +397,29 @@ impl Config {
369397
if repo.max_lines == 0 {
370398
repo.max_lines = default_pattern_repo_max_lines();
371399
}
400+
if repo.max_rules == 0 {
401+
repo.max_rules = default_pattern_repo_max_rules();
402+
}
403+
repo.rule_patterns = repo
404+
.rule_patterns
405+
.into_iter()
406+
.map(|pattern| pattern.trim().to_string())
407+
.filter(|pattern| !pattern.is_empty())
408+
.collect();
372409

373410
normalized_pattern_repositories.push(repo);
374411
}
375412
self.pattern_repositories = normalized_pattern_repositories;
413+
414+
self.rules_files = self
415+
.rules_files
416+
.iter()
417+
.map(|pattern| pattern.trim().to_string())
418+
.filter(|pattern| !pattern.is_empty())
419+
.collect();
420+
if self.max_active_rules == 0 {
421+
self.max_active_rules = default_max_active_rules();
422+
}
376423
}
377424

378425
pub fn get_path_config(&self, file_path: &Path) -> Option<&PathConfig> {
@@ -461,46 +508,6 @@ impl Config {
461508
}
462509
}
463510

464-
#[cfg(test)]
465-
mod tests {
466-
use super::*;
467-
468-
#[test]
469-
fn normalize_clamps_values() {
470-
let mut config = Config::default();
471-
config.model = " ".to_string();
472-
config.temperature = 5.0;
473-
config.max_tokens = 0;
474-
config.min_confidence = 2.0;
475-
config.strictness = 0;
476-
config.review_profile = Some("ASSERTIVE".to_string());
477-
478-
config.normalize();
479-
480-
assert_eq!(config.model, default_model());
481-
assert_eq!(config.temperature, default_temperature());
482-
assert_eq!(config.max_tokens, default_max_tokens());
483-
assert_eq!(config.min_confidence, 1.0);
484-
assert_eq!(config.strictness, default_strictness());
485-
assert_eq!(config.review_profile.as_deref(), Some("assertive"));
486-
}
487-
488-
#[test]
489-
fn normalize_comment_types_filters_unknown_values() {
490-
let mut config = Config::default();
491-
config.comment_types = vec![
492-
" LOGIC ".to_string(),
493-
"style".to_string(),
494-
"unknown".to_string(),
495-
"STYLE".to_string(),
496-
];
497-
498-
config.normalize();
499-
500-
assert_eq!(config.comment_types, vec!["logic", "style"]);
501-
}
502-
}
503-
504511
fn default_model() -> String {
505512
"gpt-4o".to_string()
506513
}
@@ -521,6 +528,14 @@ fn default_max_diff_chars() -> usize {
521528
40000
522529
}
523530

531+
fn default_context_max_chunks() -> usize {
532+
24
533+
}
534+
535+
fn default_context_budget_chars() -> usize {
536+
24000
537+
}
538+
524539
fn default_min_confidence() -> f32 {
525540
0.0
526541
}
@@ -580,6 +595,14 @@ fn default_pattern_repo_max_lines() -> usize {
580595
200
581596
}
582597

598+
fn default_pattern_repo_max_rules() -> usize {
599+
200
600+
}
601+
602+
fn default_max_active_rules() -> usize {
603+
30
604+
}
605+
583606
fn default_true() -> bool {
584607
true
585608
}
@@ -609,3 +632,47 @@ fn normalize_comment_types(values: &[String]) -> Vec<String> {
609632
normalized
610633
}
611634
}
635+
636+
#[cfg(test)]
637+
mod tests {
638+
use super::*;
639+
640+
#[test]
641+
fn normalize_clamps_values() {
642+
let mut config = Config {
643+
model: " ".to_string(),
644+
temperature: 5.0,
645+
max_tokens: 0,
646+
min_confidence: 2.0,
647+
strictness: 0,
648+
review_profile: Some("ASSERTIVE".to_string()),
649+
..Config::default()
650+
};
651+
652+
config.normalize();
653+
654+
assert_eq!(config.model, default_model());
655+
assert_eq!(config.temperature, default_temperature());
656+
assert_eq!(config.max_tokens, default_max_tokens());
657+
assert_eq!(config.min_confidence, 1.0);
658+
assert_eq!(config.strictness, default_strictness());
659+
assert_eq!(config.review_profile.as_deref(), Some("assertive"));
660+
}
661+
662+
#[test]
663+
fn normalize_comment_types_filters_unknown_values() {
664+
let mut config = Config {
665+
comment_types: vec![
666+
" LOGIC ".to_string(),
667+
"style".to_string(),
668+
"unknown".to_string(),
669+
"STYLE".to_string(),
670+
],
671+
..Config::default()
672+
};
673+
674+
config.normalize();
675+
676+
assert_eq!(config.comment_types, vec!["logic", "style"]);
677+
}
678+
}

src/core/comment.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub struct Comment {
1010
pub file_path: PathBuf,
1111
pub line_number: usize,
1212
pub content: String,
13+
#[serde(default)]
14+
pub rule_id: Option<String>,
1315
pub severity: Severity,
1416
pub category: Category,
1517
pub suggestion: Option<String>,
@@ -148,6 +150,7 @@ impl CommentSynthesizer {
148150
file_path: raw.file_path,
149151
line_number: raw.line_number,
150152
content: raw.content,
153+
rule_id: raw.rule_id,
151154
severity,
152155
category,
153156
suggestion: raw.suggestion,
@@ -470,6 +473,7 @@ pub struct RawComment {
470473
pub file_path: PathBuf,
471474
pub line_number: usize,
472475
pub content: String,
476+
pub rule_id: Option<String>,
473477
pub suggestion: Option<String>,
474478
pub severity: Option<Severity>,
475479
pub category: Option<Category>,

src/core/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod git;
77
pub mod interactive;
88
pub mod pr_summary;
99
pub mod prompt;
10+
pub mod rules;
1011
pub mod smart_review_prompt;
1112
pub mod symbol_index;
1213

@@ -18,5 +19,6 @@ pub use diff_parser::{DiffParser, UnifiedDiff};
1819
pub use git::GitIntegration;
1920
pub use pr_summary::{PRSummaryGenerator, SummaryOptions};
2021
pub use prompt::PromptBuilder;
22+
pub use rules::{active_rules_for_file, load_rules_from_patterns, ReviewRule};
2123
pub use smart_review_prompt::SmartReviewPromptBuilder;
2224
pub use symbol_index::SymbolIndex;

0 commit comments

Comments
 (0)