Skip to content

Commit 31a662f

Browse files
haasonsaasclaude
andcommitted
Add multi-pass specialized reviews, convention learning, PR summaries, OTel tracing, web tests, and Helm hardening
- Multi-pass specialized review pipeline: separate LLM passes for security, correctness, and style with per-pass system prompts and deduplication - Convention learner integration: load/save convention store, suppress previously-dismissed comment patterns - PR summary generation: generate_summary_with_commits avoids holding non-Sync GitIntegration across await boundaries - Code fix suggestions: parse <<<ORIGINAL/>>>SUGGESTED blocks from LLM responses into structured CodeSuggestion fields - OpenTelemetry tracing: feature-gated (otel) with OTLP exporter, tracing::instrument on key API handlers and webhook flows - Config additions: multi_pass_specialized, convention_store_path fields - Web UI tests: 35 vitest tests for SeverityBadge, ScoreGauge, CommentCard with testing-library/react and jsdom - Helm hardening: NetworkPolicy and PodDisruptionBudget templates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e5a95b5 commit 31a662f

File tree

26 files changed

+2796
-92
lines changed

26 files changed

+2796
-92
lines changed

Cargo.lock

Lines changed: 361 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ sha2 = "0.10"
4141
jsonwebtoken = "9"
4242
base64 = "0.22"
4343
futures = "0.3"
44+
opentelemetry = { version = "0.27", features = ["trace"], optional = true }
45+
opentelemetry-otlp = { version = "0.27", features = ["trace", "tonic"], optional = true }
46+
opentelemetry_sdk = { version = "0.27", features = ["rt-tokio"], optional = true }
47+
tracing-opentelemetry = { version = "0.28", optional = true }
48+
49+
[features]
50+
otel = ["opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "tracing-opentelemetry"]
4451

4552
[dev-dependencies]
4653
tempfile = "3.8"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{{- if .Values.networkPolicy.enabled }}
2+
apiVersion: networking.k8s.io/v1
3+
kind: NetworkPolicy
4+
metadata:
5+
name: {{ include "diffscope.fullname" . }}
6+
labels:
7+
{{- include "diffscope.labels" . | nindent 4 }}
8+
spec:
9+
podSelector:
10+
matchLabels:
11+
{{- include "diffscope.selectorLabels" . | nindent 6 }}
12+
policyTypes:
13+
- Ingress
14+
- Egress
15+
ingress:
16+
- ports:
17+
- port: {{ .Values.service.port }}
18+
protocol: TCP
19+
{{- with .Values.networkPolicy.ingressFrom }}
20+
from:
21+
{{- toYaml . | nindent 8 }}
22+
{{- end }}
23+
egress:
24+
# Allow DNS resolution (UDP + TCP port 53)
25+
- ports:
26+
- port: 53
27+
protocol: UDP
28+
- port: 53
29+
protocol: TCP
30+
# Allow HTTPS egress for LLM APIs (OpenAI, Anthropic, etc.) and GitHub
31+
- ports:
32+
- port: 443
33+
protocol: TCP
34+
{{- with .Values.networkPolicy.extraEgress }}
35+
{{- toYaml . | nindent 4 }}
36+
{{- end }}
37+
{{- end }}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{- if .Values.podDisruptionBudget.enabled }}
2+
apiVersion: policy/v1
3+
kind: PodDisruptionBudget
4+
metadata:
5+
name: {{ include "diffscope.fullname" . }}
6+
labels:
7+
{{- include "diffscope.labels" . | nindent 4 }}
8+
spec:
9+
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
10+
selector:
11+
matchLabels:
12+
{{- include "diffscope.selectorLabels" . | nindent 6 }}
13+
{{- end }}

src/adapters/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ impl Default for RetryConfig {
2626
}
2727

2828
/// Send an HTTP request with configurable retry parameters.
29+
#[tracing::instrument(name = "llm_request", skip(retry_config, make_request), fields(adapter = %adapter_name, max_retries = retry_config.max_retries))]
2930
pub async fn send_with_retry_config<F>(
3031
adapter_name: &str,
3132
retry_config: &RetryConfig,

src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ pub struct Config {
9898
#[serde(default = "default_feedback_path")]
9999
pub feedback_path: PathBuf,
100100

101+
/// Path to the convention store file for learned review patterns.
102+
/// Defaults to ~/.local/share/diffscope/conventions.json if not set.
103+
#[serde(default)]
104+
pub convention_store_path: Option<String>,
105+
101106
pub system_prompt: Option<String>,
102107
pub api_key: Option<String>,
103108
pub base_url: Option<String>,
@@ -221,6 +226,11 @@ pub struct Config {
221226
/// Webhook secret for verifying GitHub webhook signatures.
222227
#[serde(default)]
223228
pub github_webhook_secret: Option<String>,
229+
230+
/// When true, run separate specialized LLM passes for security, correctness,
231+
/// and style instead of a single monolithic review prompt.
232+
#[serde(default = "default_false")]
233+
pub multi_pass_specialized: bool,
224234
}
225235

226236
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -316,6 +326,7 @@ impl Default for Config {
316326
symbol_index_lsp_command: None,
317327
symbol_index_lsp_languages: default_symbol_index_lsp_languages(),
318328
feedback_path: default_feedback_path(),
329+
convention_store_path: None,
319330
system_prompt: None,
320331
api_key: None,
321332
base_url: None,
@@ -352,6 +363,7 @@ impl Default for Config {
352363
github_client_secret: None,
353364
github_private_key: None,
354365
github_webhook_secret: None,
366+
multi_pass_specialized: false,
355367
}
356368
}
357369
}
@@ -1074,6 +1086,10 @@ fn default_true() -> bool {
10741086
true
10751087
}
10761088

1089+
fn default_false() -> bool {
1090+
false
1091+
}
1092+
10771093
fn normalize_comment_types(values: &[String]) -> Vec<String> {
10781094
if values.is_empty() {
10791095
return default_comment_types();

src/core/comment.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,12 @@ impl CommentSynthesizer {
375375
}
376376

377377
fn generate_code_suggestion(raw: &RawComment) -> Option<CodeSuggestion> {
378-
// This is a simplified implementation - in practice, you'd use the LLM
379-
// to generate more sophisticated code suggestions
378+
// Prefer the structured code suggestion parsed from the LLM response
379+
if let Some(cs) = &raw.code_suggestion {
380+
return Some(cs.clone());
381+
}
382+
383+
// Fallback: generate a basic suggestion from the textual suggestion field
380384
if let Some(suggestion) = &raw.suggestion {
381385
if suggestion.contains("use") || suggestion.contains("replace") {
382386
return Some(CodeSuggestion {
@@ -537,6 +541,7 @@ pub struct RawComment {
537541
pub confidence: Option<f32>,
538542
pub fix_effort: Option<FixEffort>,
539543
pub tags: Vec<String>,
544+
pub code_suggestion: Option<CodeSuggestion>,
540545
}
541546

542547
#[cfg(test)]

src/core/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub use enhanced_review::{
3232
};
3333
pub use git::{validate_ref_name, GitIntegration};
3434
pub use pr_summary::{PRSummaryGenerator, SummaryOptions};
35-
pub use prompt::PromptBuilder;
35+
pub use prompt::{PromptBuilder, SpecializedPassKind};
3636
pub use rules::{active_rules_for_file, load_rules_from_patterns, ReviewRule};
3737
pub use smart_review_prompt::SmartReviewPromptBuilder;
3838
pub use symbol_index::SymbolIndex;

src/core/multi_pass.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ fn analyze_file_risk(diff: &UnifiedDiff) -> HotspotResult {
299299
}
300300

301301
/// Simple content similarity based on shared words.
302-
fn content_similarity(a: &str, b: &str) -> f32 {
302+
pub fn content_similarity(a: &str, b: &str) -> f32 {
303303
let words_a: std::collections::HashSet<&str> = a.split_whitespace().collect();
304304
let words_b: std::collections::HashSet<&str> = b.split_whitespace().collect();
305305

src/core/pr_summary.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,21 @@ impl PRSummaryGenerator {
2525
adapter: &dyn LLMAdapter,
2626
options: SummaryOptions,
2727
) -> Result<PRSummary> {
28-
// Get commit messages for context
2928
let commits = git.get_recent_commits(10)?;
29+
Self::generate_summary_with_commits(diffs, &commits, adapter, options).await
30+
}
3031

31-
// Analyze changes
32+
/// Like `generate_summary_with_options`, but takes pre-fetched commit messages
33+
/// instead of a `GitIntegration` reference. This avoids holding `GitIntegration`
34+
/// (which is not `Sync`) across an `.await` boundary.
35+
pub async fn generate_summary_with_commits(
36+
diffs: &[UnifiedDiff],
37+
commits: &[String],
38+
adapter: &dyn LLMAdapter,
39+
options: SummaryOptions,
40+
) -> Result<PRSummary> {
3241
let stats = Self::calculate_stats(diffs);
33-
34-
// Build prompt for AI summary
35-
let prompt = Self::build_summary_prompt(diffs, &commits, &stats, &options);
42+
let prompt = Self::build_summary_prompt(diffs, commits, &stats, &options);
3643

3744
let request = LLMRequest {
3845
system_prompt: Self::get_system_prompt(),
@@ -42,8 +49,6 @@ impl PRSummaryGenerator {
4249
};
4350

4451
let response = adapter.complete(request).await?;
45-
46-
// Parse AI response into structured summary
4752
Self::parse_summary_response(&response.content, stats)
4853
}
4954

0 commit comments

Comments
 (0)