Skip to content

Commit de2378f

Browse files
committed
feat: add path-scoped reinforcement buckets
1 parent 987bf88 commit de2378f

File tree

5 files changed

+89
-4
lines changed

5 files changed

+89
-4
lines changed

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ This roadmap is derived from deep research into Greptile's public docs, blog, MC
2828
4. [ ] Separate false-positive rejections from "valid but won't fix" dismissals in stored feedback.
2929
5. [ ] Weight reinforcement by reviewer role or trust level when GitHub identity is available.
3030
6. [ ] Add rule-level reinforcement decay so old team preferences do not dominate forever.
31-
7. [ ] Add path-scoped reinforcement buckets so teams can prefer different standards in `tests/`, `scripts/`, and production code.
31+
7. [x] Add path-scoped reinforcement buckets so teams can prefer different standards in `tests/`, `scripts/`, and production code.
3232
8. [ ] Persist explanation text from follow-up feedback replies and mine it into reusable review guidance.
3333
9. [ ] Learn "preferred phrasing" for accepted comments so comment tone and specificity improve over time.
3434
10. [ ] Backfill existing stored reviews into the new outcome-aware feedback store for cold-start reduction.
@@ -177,4 +177,5 @@ This roadmap is derived from deep research into Greptile's public docs, blog, MC
177177
- [x] Add pattern repository resolution coverage for repo-local directories, Git sources, and broken-source skips.
178178
- [x] Group ReviewView list-mode findings into unresolved, fixed, stale, and informational sections.
179179
- [x] Add ReviewView keyboard shortcuts for next-finding navigation plus accept/reject/resolve actions.
180+
- [x] Add path-scoped reinforcement buckets so feedback can distinguish `tests/**`, `scripts/**`, and broader code areas.
180181
- [ ] Commit and push each validated checkpoint before moving to the next epic.

src/review/feedback.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,18 @@ mod tests {
4343
}
4444

4545
#[test]
46-
fn derive_file_patterns_prefers_specific_suffixes() {
46+
fn derive_file_patterns_adds_path_scopes_before_suffixes() {
4747
let patterns = derive_file_patterns(Path::new("web/src/Settings.test.ts"));
48-
assert_eq!(patterns, vec!["*.test.ts", "*.ts"]);
48+
assert_eq!(
49+
patterns,
50+
vec!["web/src/**", "web/**", "src/**", "*.test.ts", "*.ts"]
51+
);
52+
}
53+
54+
#[test]
55+
fn derive_file_patterns_supports_extensionless_path_scopes() {
56+
let patterns = derive_file_patterns(Path::new("scripts/release"));
57+
assert_eq!(patterns, vec!["scripts/**"]);
4958
}
5059

5160
#[test]

src/review/feedback/patterns.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
use std::path::Path;
1+
use std::path::{Component, Path};
22

33
pub fn derive_file_patterns(path: &Path) -> Vec<String> {
4+
let mut patterns = derive_directory_scope_patterns(path);
5+
patterns.extend(derive_suffix_patterns(path));
6+
patterns
7+
}
8+
9+
fn derive_suffix_patterns(path: &Path) -> Vec<String> {
410
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
511
return Vec::new();
612
};
@@ -20,3 +26,39 @@ pub fn derive_file_patterns(path: &Path) -> Vec<String> {
2026

2127
patterns
2228
}
29+
30+
fn derive_directory_scope_patterns(path: &Path) -> Vec<String> {
31+
let Some(parent) = path.parent() else {
32+
return Vec::new();
33+
};
34+
35+
let segments = parent
36+
.components()
37+
.filter_map(|component| match component {
38+
Component::Normal(segment) => Some(segment.to_string_lossy().into_owned()),
39+
_ => None,
40+
})
41+
.collect::<Vec<_>>();
42+
43+
if segments.is_empty() {
44+
return Vec::new();
45+
}
46+
47+
let mut patterns = Vec::new();
48+
49+
for end in (1..=segments.len()).rev() {
50+
push_unique(&mut patterns, format!("{}/**", segments[..end].join("/")));
51+
}
52+
53+
for start in 1..segments.len() {
54+
push_unique(&mut patterns, format!("{}/**", segments[start..].join("/")));
55+
}
56+
57+
patterns
58+
}
59+
60+
fn push_unique(patterns: &mut Vec<String>, pattern: String) {
61+
if !patterns.contains(&pattern) {
62+
patterns.push(pattern);
63+
}
64+
}

src/review/feedback/record.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,16 @@ mod tests {
8080

8181
assert!(store.accept.contains(&comment.id));
8282
assert_eq!(store.by_category["Security"].accepted, 1);
83+
assert_eq!(store.by_file_pattern["src/**"].accepted, 1);
84+
assert_eq!(
85+
store.by_category_file_pattern["Security|src/**"].accepted,
86+
1
87+
);
8388
assert_eq!(store.by_rule["sec.sql.injection"].accepted, 1);
89+
assert_eq!(
90+
store.by_rule_file_pattern["sec.sql.injection|src/**"].accepted,
91+
1
92+
);
8493
assert_eq!(
8594
store.by_rule_file_pattern["sec.sql.injection|*.rs"].accepted,
8695
1

src/review/filters.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,30 @@ mod tests {
462462
);
463463
}
464464

465+
#[test]
466+
fn feedback_confidence_prefers_path_scoped_buckets() {
467+
let mut feedback = FeedbackStore::default();
468+
for _ in 0..10 {
469+
feedback.record_feedback("Bug", None, true);
470+
feedback.record_feedback_patterns("Bug", &["tests/**"], false);
471+
}
472+
473+
let mut comment = build_comment(
474+
"c1",
475+
core::comment::Category::Bug,
476+
core::comment::Severity::Error,
477+
0.8,
478+
);
479+
comment.file_path = PathBuf::from("tests/unit/parser.rs");
480+
481+
let result = apply_feedback_confidence_adjustment(vec![comment], &feedback, 5);
482+
assert!(
483+
(result[0].confidence - 0.6).abs() < 0.01,
484+
"Expected path-scoped rejection history to win, got {}",
485+
result[0].confidence
486+
);
487+
}
488+
465489
#[test]
466490
fn feedback_confidence_clamped_to_one() {
467491
let mut feedback = FeedbackStore::default();

0 commit comments

Comments
 (0)