Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3433,7 +3433,24 @@ pub fn review_batch_canonical_evidence(
evidence: Option<&str>,
) -> Option<String> {
let evidence = evidence.filter(|value| !value.trim().is_empty())?;
let payloads = review_batch_evidence_payloads(annotated, path, line);
if let Some(exact) = payloads.iter().find(|payload| **payload == evidence) {
return Some((*exact).to_string());
}
let trimmed_matches = payloads
.into_iter()
.filter(|payload| payload.trim() == evidence.trim())
.collect::<Vec<_>>();
let first = *trimmed_matches.first()?;
trimmed_matches
.iter()
.all(|payload| *payload == first)
.then(|| first.to_string())
}

fn review_batch_evidence_payloads<'a>(annotated: &'a str, path: &str, line: u32) -> Vec<&'a str> {
let mut current_path: Option<&str> = None;
let mut payloads = Vec::new();
for rendered in annotated.lines() {
if let Some(header) = rendered.strip_prefix("### ") {
current_path = Some(prompt_header_path(header));
Expand All @@ -3451,11 +3468,27 @@ pub fn review_batch_canonical_evidence(
let payload = marked
.strip_prefix("+ ")
.or_else(|| marked.strip_prefix(" "));
if let Some(payload) = payload.filter(|value| value.trim() == evidence.trim()) {
return Some(payload.to_string());
if let Some(payload) = payload.filter(|value| !value.trim().is_empty()) {
payloads.push(payload);
}
}
None
payloads
}

/// Resolve a prompt citation to the exact non-empty new-side text the model
/// must copy. This is exposed to the correction prompt, while final acceptance
/// continues to require an exact match through `review_batch_canonical_evidence`.
pub fn review_batch_expected_evidence(annotated: &str, path: &str, line: u32) -> Option<String> {
let payloads = review_batch_evidence_payloads(annotated, path, line);
let first = *payloads.first()?;
payloads
.iter()
.all(|payload| *payload == first)
.then(|| first.to_string())
}

pub fn review_batch_has_evidence_anchor(annotated: &str, path: &str, line: u32) -> bool {
!review_batch_evidence_payloads(annotated, path, line).is_empty()
}

/// Render a bounded local window around a citation from the exact evidence a
Expand Down Expand Up @@ -4135,12 +4168,59 @@ Binary files a/img.png and b/img.png differ
.as_deref(),
Some(" indented replacement")
);
assert_eq!(
review_batch_expected_evidence(batch, "src/a.rs", 10).as_deref(),
Some("replacement command")
);
assert_eq!(
review_batch_expected_evidence(batch, "src/a.rs", 13).as_deref(),
Some(" indented replacement")
);
assert_eq!(review_batch_expected_evidence(batch, "src/a.rs", 11), None);
assert_eq!(review_batch_expected_evidence(batch, "src/a.rs", 99), None);
assert!(!review_batch_contains_exact_evidence(
batch,
"src/a.rs",
13,
Some("indented replacement")
));

let repeated = "### src/a.rs\n@@ first @@\n 13 + first slice\n@@ second @@\n 13 + second slice\n";
assert_eq!(
review_batch_expected_evidence(repeated, "src/a.rs", 13),
None
);
assert!(review_batch_has_evidence_anchor(repeated, "src/a.rs", 13));
assert_eq!(
review_batch_canonical_evidence(repeated, "src/a.rs", 13, Some("second slice"))
.as_deref(),
Some("second slice")
);

let whitespace_distinct = "### src/a.rs\n@@ first @@\n 13 + changed();\n@@ second @@\n 13 + changed();\n";
assert_eq!(
review_batch_canonical_evidence(
whitespace_distinct,
"src/a.rs",
13,
Some("changed();")
),
None
);
assert_eq!(
review_batch_canonical_evidence(
whitespace_distinct,
"src/a.rs",
13,
Some(" changed();")
)
.as_deref(),
Some(" changed();")
);
assert_eq!(
review_batch_expected_evidence(whitespace_distinct, "src/a.rs", 13),
None
);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ fn review_semantic_retry_user(user: &str, previous: &str) -> String {

fn review_validation_retry_user(user: &str, reason: &str) -> String {
format!(
"{user}\n\n[Correction] The previous response was unusable ({reason}). Retry once. Every finding must cite an exact path and new-file line displayed in the review input. Return only the corrected review JSON."
"{user}\n\n[Correction] The previous response was unusable: {reason}. Correct that exact contract failure. Preserve a finding only when the review input supports it; otherwise retract it. Return only the corrected review JSON."
)
}

Expand Down
175 changes: 143 additions & 32 deletions src/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,65 @@ fn conservative_context_tokens(model: &str) -> usize {
}
}

fn review_batch_validation_reason(
finding: &Finding,
annotated: &str,
content_policy_prompt: Option<&str>,
) -> Option<String> {
if let Err(reason) = crate::envelope::validate_finding_publication(finding) {
return Some(format!(
"finding at {}:{} violates the publication contract: {reason}",
finding.path, finding.line
));
}

if diff::review_batch_contains_exact_evidence(
annotated,
&finding.path,
finding.line,
finding.evidence.as_deref(),
) || content_policy_prompt.is_some_and(|prompt| {
diff::review_batch_contains_exact_evidence(
prompt,
&finding.path,
finding.line,
finding.evidence.as_deref(),
)
}) {
return None;
}

let evidence_source = content_policy_prompt
.filter(|prompt| {
diff::review_batch_has_evidence_anchor(prompt, &finding.path, finding.line)
})
.or_else(|| {
diff::review_batch_has_evidence_anchor(annotated, &finding.path, finding.line)
.then_some(annotated)
});

let Some(evidence_source) = evidence_source else {
return Some(format!(
"finding at {}:{} does not cite a non-empty new-side line displayed in this review input; retract it or cite a displayed new-side line",
finding.path, finding.line
));
};
let Some(expected) =
diff::review_batch_expected_evidence(evidence_source, &finding.path, finding.line)
else {
return Some(format!(
"finding at {}:{} has multiple displayed new-side evidence strings; copy the exact supporting string or retract it",
finding.path, finding.line
));
};
Some(format!(
"finding at {}:{} must set `evidence` to the exact JSON string {}",
finding.path,
finding.line,
serde_json::to_string(&expected).expect("evidence string is JSON-serializable")
))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForgeKind {
GitHub,
Expand Down Expand Up @@ -1147,31 +1206,20 @@ async fn review_diff(cfg: &Config, args: &ReviewArgs, input: ReviewInput<'_>) ->
let validation_user = user.clone();
match client
.review_validated(cfg, &system, &user, move |review| {
let invalid = review.findings.iter().find(|finding| {
let grounded = diff::review_batch_contains_exact_evidence(
&validation_annotated,
&finding.path,
finding.line,
finding.evidence.as_deref(),
) || (first
&& finding.kind == crate::envelope::Kind::ContentPolicy
&& diff::review_batch_contains_exact_evidence(
&validation_user,
&finding.path,
finding.line,
finding.evidence.as_deref(),
));
crate::envelope::validate_finding_publication(finding).is_err()
|| !grounded
});
if let Some(finding) = invalid {
Err(format!(
"finding at {}:{} has invalid publication text or lacks exact new-side evidence",
finding.path, finding.line
))
} else {
Ok(())
}
review
.findings
.iter()
.find_map(|finding| {
review_batch_validation_reason(
finding,
&validation_annotated,
(first
&& finding.kind
== crate::envelope::Kind::ContentPolicy)
.then_some(validation_user.as_str()),
)
})
.map_or(Ok(()), Err)
})
.await
{
Expand Down Expand Up @@ -1210,15 +1258,13 @@ async fn review_diff(cfg: &Config, args: &ReviewArgs, input: ReviewInput<'_>) ->
finding.evidence.as_deref(),
)
.or_else(|| {
(first
&& finding.kind
== crate::envelope::Kind::ContentPolicy)
(first && finding.kind == crate::envelope::Kind::ContentPolicy)
.then(|| {
diff::review_batch_canonical_evidence(
&user,
&finding.path,
finding.line,
finding.evidence.as_deref(),
&user,
&finding.path,
finding.line,
finding.evidence.as_deref(),
)
})
.flatten()
Expand Down Expand Up @@ -2279,4 +2325,69 @@ mod tests {
assert_eq!(findings[0].confidence, 0.49);
assert_eq!(findings[0].kind, Kind::Uncertainty);
}

#[test]
fn batch_validation_exposes_exact_evidence_for_one_correction() {
let annotated = "### src/lib.rs\n@@ fixture @@\n 7 + changed();\n";
let mut finding = finding("src/lib.rs", 7, "This change is unsafe.");
finding.evidence = Some("changed approximately".to_string());

let reason = review_batch_validation_reason(&finding, annotated, None).unwrap();
assert_eq!(
reason,
"finding at src/lib.rs:7 must set `evidence` to the exact JSON string \" changed();\""
);

finding.evidence = Some(" changed();".to_string());
assert_eq!(
review_batch_validation_reason(&finding, annotated, None),
None
);
}

#[test]
fn batch_validation_reports_publication_failure_before_grounding() {
let annotated = "### src/lib.rs\n@@ fixture @@\n 7 + changed();\n";
let mut finding = finding("src/lib.rs", 7, "This sentence is cut off");
finding.evidence = Some("changed();".to_string());

assert_eq!(
review_batch_validation_reason(&finding, annotated, None).as_deref(),
Some(
"finding at src/lib.rs:7 violates the publication contract: finding body must end with sentence punctuation"
)
);
}

#[test]
fn content_policy_evidence_may_come_from_policy_prompt_on_an_overlapping_line() {
let annotated = "### .postil/content-policy.md\n@@ diff @@\n 7 + repository text\n";
let policy = "### .postil/content-policy.md\n@@ policy @@\n 7 + policy text\n";
let mut finding = finding(
".postil/content-policy.md",
7,
"The proposed text violates the configured policy.",
);
finding.kind = Kind::ContentPolicy;
finding.evidence = Some("policy text".to_string());

assert_eq!(
review_batch_validation_reason(&finding, annotated, Some(policy)),
None
);
}

#[test]
fn batch_validation_does_not_choose_between_distinct_duplicate_slices() {
let annotated = "### src/lib.rs\n@@ first @@\n 7 + first slice\n@@ second @@\n 7 + second slice\n";
let mut finding = finding("src/lib.rs", 7, "This change is unsafe.");
finding.evidence = Some("approximate evidence".to_string());

assert_eq!(
review_batch_validation_reason(&finding, annotated, None).as_deref(),
Some(
"finding at src/lib.rs:7 has multiple displayed new-side evidence strings; copy the exact supporting string or retract it"
)
);
}
}
Loading
Loading