Skip to content

Commit d4775ba

Browse files
committed
refactor: split review response validation helpers
Made-with: Cursor
1 parent e45c9e9 commit d4775ba

File tree

4 files changed

+65
-51
lines changed

4 files changed

+65
-51
lines changed

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
### Review pipeline backlog
9797

9898
- [x] `src/review/pipeline/execution/responses/processing.rs`: split raw response normalization, comment extraction, and merge logic.
99-
- [ ] `src/review/pipeline/execution/responses/validation.rs`: separate schema validation from per-comment sanitization.
99+
- [x] `src/review/pipeline/execution/responses/validation.rs`: separate schema validation from per-comment sanitization.
100100
- [ ] `src/review/pipeline/prepare/runner.rs`: split per-diff orchestration, pre-analysis/triage decisions, and progress updates.
101101
- [ ] `src/review/pipeline/context/symbols.rs`: split symbol search, snippet selection, and fallback behavior.
102102
- [ ] `src/review/pipeline/context/related.rs`: separate related-file discovery from ranking/selection.

src/review/pipeline/execution/responses/validation.rs

Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
#[path = "validation/contract.rs"]
2+
mod contract;
3+
#[path = "validation/repetition.rs"]
4+
mod repetition;
5+
6+
use contract::validate_structured_review_payload;
7+
use repetition::has_excessive_repetition;
8+
19
pub(super) fn validate_llm_response(response: &str) -> Result<(), String> {
210
let trimmed = response.trim();
311
if trimmed.is_empty() {
412
return Err("Empty response from model".to_string());
513
}
614

7-
if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
8-
if is_structured_review_payload(&value) {
9-
return Ok(());
10-
}
11-
12-
return Err("JSON response did not match the review output contract".to_string());
15+
if validate_structured_review_payload(trimmed)? {
16+
return Ok(());
1317
}
1418

1519
if response.len() < 10 {
@@ -23,50 +27,6 @@ pub(super) fn validate_llm_response(response: &str) -> Result<(), String> {
2327
Ok(())
2428
}
2529

26-
fn is_structured_review_payload(value: &serde_json::Value) -> bool {
27-
let items = if let Some(array) = value.as_array() {
28-
array
29-
} else if let Some(array) = value
30-
.get("comments")
31-
.or_else(|| value.get("findings"))
32-
.or_else(|| value.get("results"))
33-
.and_then(|items| items.as_array())
34-
{
35-
array
36-
} else {
37-
return false;
38-
};
39-
40-
items.iter().all(|item| {
41-
item.is_object()
42-
&& (item.get("line").is_some()
43-
|| item.get("line_number").is_some()
44-
|| item.get("content").is_some()
45-
|| item.get("issue").is_some())
46-
})
47-
}
48-
49-
pub(super) fn has_excessive_repetition(text: &str) -> bool {
50-
if text.len() < 100 {
51-
return false;
52-
}
53-
let window = 20.min(text.len() / 5);
54-
let search_end = text.len().saturating_sub(window);
55-
for start in 0..search_end.max(1) {
56-
if !text.is_char_boundary(start) || !text.is_char_boundary(start + window) {
57-
continue;
58-
}
59-
let pattern = &text[start..start + window];
60-
if pattern.trim().is_empty() {
61-
continue;
62-
}
63-
if text.matches(pattern).count() > 5 {
64-
return true;
65-
}
66-
}
67-
false
68-
}
69-
7030
#[cfg(test)]
7131
mod tests {
7232
use super::*;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
pub(super) fn validate_structured_review_payload(response: &str) -> Result<bool, String> {
2+
let Ok(value) = serde_json::from_str::<serde_json::Value>(response) else {
3+
return Ok(false);
4+
};
5+
6+
if is_structured_review_payload(&value) {
7+
return Ok(true);
8+
}
9+
10+
Err("JSON response did not match the review output contract".to_string())
11+
}
12+
13+
fn is_structured_review_payload(value: &serde_json::Value) -> bool {
14+
let items = if let Some(array) = value.as_array() {
15+
array
16+
} else if let Some(array) = value
17+
.get("comments")
18+
.or_else(|| value.get("findings"))
19+
.or_else(|| value.get("results"))
20+
.and_then(|items| items.as_array())
21+
{
22+
array
23+
} else {
24+
return false;
25+
};
26+
27+
items.iter().all(|item| {
28+
item.is_object()
29+
&& (item.get("line").is_some()
30+
|| item.get("line_number").is_some()
31+
|| item.get("content").is_some()
32+
|| item.get("issue").is_some())
33+
})
34+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
pub(super) fn has_excessive_repetition(text: &str) -> bool {
2+
if text.len() < 100 {
3+
return false;
4+
}
5+
let window = 20.min(text.len() / 5);
6+
let search_end = text.len().saturating_sub(window);
7+
for start in 0..search_end.max(1) {
8+
if !text.is_char_boundary(start) || !text.is_char_boundary(start + window) {
9+
continue;
10+
}
11+
let pattern = &text[start..start + window];
12+
if pattern.trim().is_empty() {
13+
continue;
14+
}
15+
if text.matches(pattern).count() > 5 {
16+
return true;
17+
}
18+
}
19+
false
20+
}

0 commit comments

Comments
 (0)