Skip to content

Commit 4abd814

Browse files
committed
feat(analytics): track pattern repository impact
1 parent f247187 commit 4abd814

File tree

6 files changed

+572
-5
lines changed

6 files changed

+572
-5
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ This roadmap is derived from deep research into Greptile's public docs, blog, MC
106106
64. [x] Add unresolved blocker counts per repository and per PR.
107107
65. [x] Add review completeness and mean-time-to-resolution charts.
108108
66. [x] Add feedback-learning effectiveness metrics: did reranked findings get higher acceptance after rollout?
109-
67. [ ] Add pattern-repository utilization analytics showing when extra context actually affected findings.
109+
67. [x] Add pattern-repository utilization analytics showing when extra context actually affected findings.
110110
68. [x] Add eval-vs-production dashboards comparing benchmark strength against real-world acceptance.
111111
69. [x] Add drill-downs from trend charts directly into the affected reviews, findings, and rules.
112112
70. [x] Add exportable JSON/CSV reports for review quality, lifecycle, and reinforcement metrics.

src/review/rule_helpers/runtime.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,58 @@ mod tests {
9090
let result = apply_rule_overrides(vec![comment], &rules);
9191
assert_eq!(result[0].severity, core::comment::Severity::Info);
9292
}
93+
94+
#[test]
95+
fn apply_rule_overrides_tags_pattern_repository_sources() {
96+
let mut comment = build_comment(
97+
"c1",
98+
core::comment::Category::Bug,
99+
core::comment::Severity::Info,
100+
0.5,
101+
);
102+
comment.rule_id = Some("sec.sql.injection".to_string());
103+
104+
let rules = vec![core::ReviewRule {
105+
source: "https://github.com/acme/security-rules.git".to_string(),
106+
id: "sec.sql.injection".to_string(),
107+
description: "Parameterized queries required".to_string(),
108+
severity: None,
109+
category: None,
110+
scope: None,
111+
tags: vec![],
112+
}];
113+
114+
let result = apply_rule_overrides(vec![comment], &rules);
115+
assert!(result[0].tags.contains(&"pattern-repository".to_string()));
116+
assert!(result[0]
117+
.tags
118+
.contains(&"pattern-repository:acme/security-rules".to_string()));
119+
}
120+
121+
#[test]
122+
fn apply_rule_overrides_skips_repository_source_tag_for_local_rules() {
123+
let mut comment = build_comment(
124+
"c1",
125+
core::comment::Category::Bug,
126+
core::comment::Severity::Info,
127+
0.5,
128+
);
129+
comment.rule_id = Some("local.rule".to_string());
130+
131+
let rules = vec![core::ReviewRule {
132+
source: "repository".to_string(),
133+
id: "local.rule".to_string(),
134+
description: "Repo-local rule".to_string(),
135+
severity: None,
136+
category: None,
137+
scope: None,
138+
tags: vec![],
139+
}];
140+
141+
let result = apply_rule_overrides(vec![comment], &rules);
142+
assert!(!result[0]
143+
.tags
144+
.iter()
145+
.any(|tag| tag.starts_with("pattern-repository")));
146+
}
93147
}

src/review/rule_helpers/runtime/overrides.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::path::Path;
23

34
use crate::core;
45

@@ -67,9 +68,77 @@ fn apply_rule_tags(comment: &mut core::Comment, rule: &core::ReviewRule) {
6768
if !comment.tags.iter().any(|tag| tag == &marker) {
6869
comment.tags.push(marker);
6970
}
71+
72+
if let Some(pattern_repository_tag) = pattern_repository_source_tag(&rule.source) {
73+
if !comment
74+
.tags
75+
.iter()
76+
.any(|existing| existing == "pattern-repository")
77+
{
78+
comment.tags.push("pattern-repository".to_string());
79+
}
80+
if !comment
81+
.tags
82+
.iter()
83+
.any(|existing| existing == &pattern_repository_tag)
84+
{
85+
comment.tags.push(pattern_repository_tag);
86+
}
87+
}
88+
7089
for tag in &rule.tags {
7190
if !comment.tags.iter().any(|existing| existing == tag) {
7291
comment.tags.push(tag.clone());
7392
}
7493
}
7594
}
95+
96+
fn pattern_repository_source_tag(source: &str) -> Option<String> {
97+
let trimmed = source.trim();
98+
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("repository") {
99+
return None;
100+
}
101+
102+
Some(format!(
103+
"pattern-repository:{}",
104+
pattern_repository_source_label(trimmed)
105+
))
106+
}
107+
108+
fn pattern_repository_source_label(source: &str) -> String {
109+
let trimmed = source.trim().trim_end_matches('/').trim_end_matches(".git");
110+
111+
if !trimmed.contains("://") && !trimmed.contains('@') {
112+
let parts = trimmed
113+
.split('/')
114+
.filter(|part| !part.is_empty())
115+
.collect::<Vec<_>>();
116+
117+
if parts.len() == 2 {
118+
return format!("{}/{}", parts[0], parts[1]);
119+
}
120+
}
121+
122+
if !trimmed.contains("://") && !trimmed.contains('@') {
123+
if let Some(name) = Path::new(trimmed)
124+
.file_name()
125+
.and_then(|value| value.to_str())
126+
{
127+
if !name.is_empty() {
128+
return name.to_string();
129+
}
130+
}
131+
}
132+
133+
let normalized = trimmed.replace(':', "/");
134+
let parts = normalized
135+
.split('/')
136+
.filter(|part| !part.is_empty() && !part.ends_with(':'))
137+
.collect::<Vec<_>>();
138+
139+
match parts.as_slice() {
140+
[.., owner, repo] => format!("{owner}/{repo}"),
141+
[single] => (*single).to_string(),
142+
[] => "external".to_string(),
143+
}
144+
}

0 commit comments

Comments
 (0)