Skip to content

Commit 09c7a7d

Browse files
committed
feat(review): add ticket intent mismatch checks
1 parent ac64579 commit 09c7a7d

File tree

4 files changed

+66
-2
lines changed

4 files changed

+66
-2
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ This roadmap is derived from deep research into Greptile's public docs, blog, MC
8080
44. [x] Add per-path scoped review instructions in the Settings UI for common repo areas.
8181
45. [x] Support Jira/Linear issue context ingestion for PR-linked reviews.
8282
46. [ ] Support document-backed context ingestion for design docs, RFCs, and runbooks.
83-
47. [ ] Add explicit "intent mismatch" review checks comparing PR changes to ticket acceptance criteria.
83+
47. [x] Add explicit "intent mismatch" review checks comparing PR changes to ticket acceptance criteria.
8484
48. [x] Add review artifacts that show which external context sources influenced a finding.
8585
49. [x] Add tests for pattern repository resolution across local paths, Git URLs, and broken sources.
8686
50. [ ] Add analytics on which context sources actually improve acceptance and fix rates.

src/review/context_helpers/injection.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub fn inject_linked_issue_context(
7474
lines.push(format!("URL: {url}"));
7575
}
7676
if !issue.summary.trim().is_empty() {
77-
lines.push("Summary:".to_string());
77+
lines.push("Acceptance criteria / ticket context:".to_string());
7878
lines.push(issue.summary.trim().to_string());
7979
}
8080

@@ -243,6 +243,9 @@ mod tests {
243243
context_chunks[1].provenance,
244244
Some(core::ContextProvenance::linear_issue_context("OPS-9"))
245245
);
246+
assert!(context_chunks[0]
247+
.content
248+
.contains("Acceptance criteria / ticket context:"));
246249
assert!(context_chunks[1]
247250
.content
248251
.contains("Deployment manifests should use the new secret name."));

src/review/pipeline/guidance.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@ mod tests {
9797
assert!(guidance.contains("Do not include code fix suggestions"));
9898
}
9999

100+
#[test]
101+
fn build_review_guidance_includes_linked_issue_intent_validation() {
102+
let config = config::Config {
103+
linked_issue_contexts: vec![config::LinkedIssueContext {
104+
provider: config::LinkedIssueProvider::Jira,
105+
identifier: "ENG-123".to_string(),
106+
title: Some("Keep API status enum aligned".to_string()),
107+
status: Some("In Progress".to_string()),
108+
url: None,
109+
summary: "The API must keep the documented pending/shipped/cancelled states."
110+
.to_string(),
111+
}],
112+
..config::Config::default()
113+
};
114+
let guidance = build_review_guidance(&config, None).unwrap();
115+
assert!(guidance.contains("Linked issue intent validation"));
116+
assert!(guidance.contains("acceptance criteria"));
117+
assert!(guidance.contains("design.ticket.intent-mismatch"));
118+
assert!(guidance.contains("intent-mismatch"));
119+
assert!(guidance.contains("Jira ENG-123: Keep API status enum aligned"));
120+
}
121+
100122
#[test]
101123
fn build_review_guidance_includes_prose_rules() {
102124
// #12: natural language custom rules — injected as bullets into guidance

src/review/pipeline/guidance/sections.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub(super) fn collect_guidance_sections(
1010
push_section(&mut sections, review_profile_section(config));
1111
push_section(&mut sections, global_instructions_section(config));
1212
push_section(&mut sections, prose_rules_section(config));
13+
push_section(&mut sections, linked_issue_validation_section(config));
1314
push_section(&mut sections, path_instructions_section(path_config));
1415
push_section(&mut sections, output_language_section(config));
1516
sections.push(fix_suggestion_section(config));
@@ -86,6 +87,44 @@ fn prose_rules_section(config: &config::Config) -> Option<String> {
8687
}
8788
}
8889

90+
fn linked_issue_validation_section(config: &config::Config) -> Option<String> {
91+
if config.linked_issue_contexts.is_empty() {
92+
return None;
93+
}
94+
95+
let issues = config
96+
.linked_issue_contexts
97+
.iter()
98+
.map(|issue| {
99+
let provider = match issue.provider {
100+
config::LinkedIssueProvider::Jira => "Jira",
101+
config::LinkedIssueProvider::Linear => "Linear",
102+
};
103+
let mut label = format!("- {provider} {}", issue.identifier);
104+
if let Some(title) = issue
105+
.title
106+
.as_deref()
107+
.filter(|value| !value.trim().is_empty())
108+
{
109+
label.push_str(&format!(": {title}"));
110+
}
111+
if let Some(status) = issue
112+
.status
113+
.as_deref()
114+
.filter(|value| !value.trim().is_empty())
115+
{
116+
label.push_str(&format!(" ({status})"));
117+
}
118+
label
119+
})
120+
.collect::<Vec<_>>()
121+
.join("\n");
122+
123+
Some(format!(
124+
"Linked issue intent validation:\n- Explicitly compare the diff against each linked issue's acceptance criteria, requirements, and promised scope before deciding there are no problems.\n- Report a finding when the change contradicts, omits, or weakens required ticket behavior, or when it introduces behavior that does not match the linked issue intent.\n- For those findings, use rule_id `design.ticket.intent-mismatch` and include the tag `intent-mismatch`.\nLinked issues in scope:\n{issues}"
125+
))
126+
}
127+
89128
fn path_instructions_section(path_config: Option<&config::PathConfig>) -> Option<String> {
90129
let instructions = path_config?.review_instructions.as_deref()?.trim();
91130
if instructions.is_empty() {

0 commit comments

Comments
 (0)