Skip to content

Commit ff62123

Browse files
committed
feat(review): ingest linked document context
1 parent 279188f commit ff62123

8 files changed

Lines changed: 564 additions & 14 deletions

File tree

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ This roadmap is derived from deep research into Greptile's public docs, blog, MC
7979
43. [x] Add structured UI editing for custom context notes, files, and scopes.
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.
82-
46. [ ] Support document-backed context ingestion for design docs, RFCs, and runbooks.
82+
46. [x] Support document-backed context ingestion for design docs, RFCs, and runbooks.
8383
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.

src/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ pub struct LinkedIssueContext {
155155
pub summary: String,
156156
}
157157

158+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159+
pub struct DocumentContext {
160+
pub source: String,
161+
pub title: String,
162+
pub url: String,
163+
#[serde(default)]
164+
pub summary: String,
165+
}
166+
158167
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
159168
pub struct AutomationConfig {
160169
/// Outbound webhook URL for downstream automation consumers.
@@ -501,6 +510,9 @@ pub struct Config {
501510
#[serde(default, skip_serializing_if = "Vec::is_empty")]
502511
pub linked_issue_contexts: Vec<LinkedIssueContext>,
503512

513+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
514+
pub document_contexts: Vec<DocumentContext>,
515+
504516
#[serde(default)]
505517
pub pattern_repositories: Vec<PatternRepositoryConfig>,
506518

@@ -728,6 +740,7 @@ impl Default for Config {
728740
paths: HashMap::new(),
729741
custom_context: Vec::new(),
730742
linked_issue_contexts: Vec::new(),
743+
document_contexts: Vec::new(),
731744
pattern_repositories: Vec::new(),
732745
rules_files: Vec::new(),
733746
max_active_rules: default_max_active_rules(),

src/core/context_provenance.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ pub enum ContextProvenance {
1010
name: String,
1111
},
1212
CustomContextNotes,
13+
DocumentContext {
14+
source: String,
15+
title: String,
16+
},
1317
JiraIssueContext {
1418
issue_key: String,
1519
},
@@ -65,6 +69,13 @@ impl ContextProvenance {
6569
}
6670
}
6771

72+
pub fn document_context(source: impl Into<String>, title: impl Into<String>) -> Self {
73+
Self::DocumentContext {
74+
source: source.into(),
75+
title: title.into(),
76+
}
77+
}
78+
6879
pub fn linear_issue_context(issue_id: impl Into<String>) -> Self {
6980
Self::LinearIssueContext {
7081
issue_id: issue_id.into(),
@@ -118,6 +129,7 @@ impl ContextProvenance {
118129
}
119130
Self::Analyzer { .. }
120131
| Self::CustomContextNotes
132+
| Self::DocumentContext { .. }
121133
| Self::JiraIssueContext { .. }
122134
| Self::LinearIssueContext { .. }
123135
| Self::DependencyGraphNeighborhood
@@ -141,6 +153,7 @@ impl ContextProvenance {
141153
let source = match self {
142154
Self::ActiveReviewRules | Self::Analyzer { .. } => return None,
143155
Self::CustomContextNotes => "custom-context".to_string(),
156+
Self::DocumentContext { source, .. } => source.trim().to_string(),
144157
Self::JiraIssueContext { .. } => "jira-issue".to_string(),
145158
Self::LinearIssueContext { .. } => "linear-issue".to_string(),
146159
Self::DependencyGraphNeighborhood => "dependency-graph".to_string(),
@@ -165,6 +178,9 @@ impl ContextProvenance {
165178
Self::ActiveReviewRules => "active review rules".to_string(),
166179
Self::Analyzer { name } => format!("{name} analyzer"),
167180
Self::CustomContextNotes => "custom context notes".to_string(),
181+
Self::DocumentContext { source, title } => {
182+
format!("document context ({source}): {title}")
183+
}
168184
Self::JiraIssueContext { issue_key } => format!("jira issue context: {issue_key}"),
169185
Self::LinearIssueContext { issue_id } => {
170186
format!("linear issue context: {issue_id}")
@@ -260,6 +276,10 @@ mod tests {
260276
ContextProvenance::RepositoryGraphMetadata.to_string(),
261277
"repository graph metadata"
262278
);
279+
assert_eq!(
280+
ContextProvenance::document_context("design-doc", "Checkout architecture").to_string(),
281+
"document context (design-doc): Checkout architecture"
282+
);
263283
assert_eq!(
264284
ContextProvenance::jira_issue_context("ENG-123").to_string(),
265285
"jira issue context: ENG-123"
@@ -290,6 +310,12 @@ mod tests {
290310
.as_deref(),
291311
Some("context-source:symbol-graph")
292312
);
313+
assert_eq!(
314+
ContextProvenance::document_context("runbook", "Pager escalation")
315+
.artifact_tag()
316+
.as_deref(),
317+
Some("context-source:runbook")
318+
);
293319
assert_eq!(
294320
ContextProvenance::jira_issue_context("ENG-123")
295321
.artifact_tag()

src/review/context_helpers.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ mod pattern_repositories;
66
mod ranking;
77

88
pub use injection::{
9-
inject_custom_context, inject_linked_issue_context, inject_pattern_repository_context,
9+
inject_custom_context, inject_document_context, inject_linked_issue_context,
10+
inject_pattern_repository_context,
1011
};
1112
pub use pattern_repositories::{resolve_pattern_repositories, PatternRepositoryMap};
1213
pub use ranking::rank_and_trim_context_chunks;

src/review/context_helpers/injection.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,45 @@ pub fn inject_linked_issue_context(
9292
}
9393
}
9494

95+
fn document_source_label(source: &str) -> &str {
96+
match source {
97+
"design-doc" => "design doc",
98+
"rfc" => "RFC",
99+
"runbook" => "runbook",
100+
_ => "document",
101+
}
102+
}
103+
104+
pub fn inject_document_context(
105+
config: &config::Config,
106+
diff: &core::UnifiedDiff,
107+
context_chunks: &mut Vec<core::LLMContextChunk>,
108+
) {
109+
for document in &config.document_contexts {
110+
let mut lines = vec![format!(
111+
"Linked {} context: {}",
112+
document_source_label(&document.source),
113+
document.title
114+
)];
115+
116+
if !document.url.trim().is_empty() {
117+
lines.push(format!("URL: {}", document.url.trim()));
118+
}
119+
if !document.summary.trim().is_empty() {
120+
lines.push("Relevant design / runbook context:".to_string());
121+
lines.push(document.summary.trim().to_string());
122+
}
123+
124+
context_chunks.push(
125+
core::LLMContextChunk::documentation(diff.file_path.clone(), lines.join("\n"))
126+
.with_provenance(core::ContextProvenance::document_context(
127+
document.source.clone(),
128+
document.title.clone(),
129+
)),
130+
);
131+
}
132+
}
133+
95134
pub async fn inject_pattern_repository_context(
96135
config: &config::Config,
97136
resolved_repositories: &PatternRepositoryMap,
@@ -251,6 +290,32 @@ mod tests {
251290
.contains("Deployment manifests should use the new secret name."));
252291
}
253292

293+
#[test]
294+
fn inject_document_context_uses_document_provenance() {
295+
let mut config = config::Config::default();
296+
config.document_contexts = vec![config::DocumentContext {
297+
source: "design-doc".to_string(),
298+
title: "Checkout resiliency RFC".to_string(),
299+
url: "https://github.com/evalops/diffscope/blob/main/docs/rfcs/checkout.md".to_string(),
300+
summary: "Keep retries idempotent and preserve queue ordering.".to_string(),
301+
}];
302+
303+
let mut context_chunks = Vec::new();
304+
inject_document_context(&config, &diff_for("src/lib.rs"), &mut context_chunks);
305+
306+
assert_eq!(context_chunks.len(), 1);
307+
assert!(context_chunks[0]
308+
.content
309+
.contains("Linked design doc context: Checkout resiliency RFC"));
310+
assert_eq!(
311+
context_chunks[0].provenance,
312+
Some(core::ContextProvenance::document_context(
313+
"design-doc",
314+
"Checkout resiliency RFC"
315+
))
316+
);
317+
}
318+
254319
#[tokio::test]
255320
async fn inject_pattern_repository_context_tags_source_and_chunks() {
256321
let dir = tempfile::tempdir().unwrap();

src/review/pipeline/file_context/sources/repo.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ pub(in super::super) async fn inject_repository_context(
1414
diff,
1515
context_chunks,
1616
);
17+
super::super::super::super::context_helpers::inject_document_context(
18+
&services.config,
19+
diff,
20+
context_chunks,
21+
);
1722
super::super::super::super::context_helpers::inject_custom_context(
1823
&services.config,
1924
&services.context_fetcher,

0 commit comments

Comments
 (0)