feat(agent): grounded control checker — compose judge -> ground gate
GroundedControlChecker<J: ControlJudge>::check(spec, regions, repo_id) judges each candidate region and keeps only findings that survive the core grounding gate. Generic over the judge so tests drive it with a deterministic stub — the recognize->ground path is proven without an LLM (grounded snippet kept, ungrounded dropped, non-violation yields nothing). 2 lib tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b0117078f3
commit
d1f42a1a83
@@ -0,0 +1,114 @@
|
||||
//! The grounded control checker: judge each candidate region for a control, then
|
||||
//! keep only the verdicts that survive the grounding gate.
|
||||
//!
|
||||
//! Generic over [`ControlJudge`] so tests drive it with a deterministic stub —
|
||||
//! the whole recognize → ground path is then exercised without an LLM. With the
|
||||
//! real judge, determinism comes from temperature 0 plus the gate.
|
||||
|
||||
use compliance_core::control_check::{ground, CandidateRegion, ControlCheckSpec};
|
||||
use compliance_core::models::Finding;
|
||||
|
||||
use super::judge::ControlJudge;
|
||||
|
||||
/// Runs a [`ControlJudge`] over candidate regions and grounds the results.
|
||||
pub struct GroundedControlChecker<J> {
|
||||
judge: J,
|
||||
}
|
||||
|
||||
impl<J: ControlJudge> GroundedControlChecker<J> {
|
||||
pub fn new(judge: J) -> Self {
|
||||
Self { judge }
|
||||
}
|
||||
|
||||
/// Judge every candidate region for `spec` and return the grounded findings.
|
||||
/// A verdict that doesn't quote real code in its region is dropped by
|
||||
/// [`ground`], so nothing fabricated reaches the caller.
|
||||
pub async fn check(
|
||||
&self,
|
||||
spec: &ControlCheckSpec,
|
||||
regions: &[CandidateRegion],
|
||||
repo_id: &str,
|
||||
) -> Vec<Finding> {
|
||||
let mut findings = Vec::new();
|
||||
for region in regions {
|
||||
let verdict = self.judge.judge(spec, region).await;
|
||||
if let Some(finding) = ground(spec, region, &verdict, repo_id) {
|
||||
findings.push(finding);
|
||||
}
|
||||
}
|
||||
findings
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use compliance_core::control_check::LlmVerdict;
|
||||
use compliance_core::models::finding::Severity;
|
||||
|
||||
/// Deterministic stub: returns a fixed verdict for every region, so the
|
||||
/// recognize → ground composition is tested without an LLM.
|
||||
struct StubJudge {
|
||||
verdict: LlmVerdict,
|
||||
}
|
||||
|
||||
impl ControlJudge for StubJudge {
|
||||
async fn judge(&self, _spec: &ControlCheckSpec, _region: &CandidateRegion) -> LlmVerdict {
|
||||
self.verdict.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn spec() -> ControlCheckSpec {
|
||||
ControlCheckSpec {
|
||||
control_id: "cra-ai-8".into(),
|
||||
title: "No default passwords".into(),
|
||||
requirement: "No default credentials".into(),
|
||||
default_cwe: Some("CWE-798".into()),
|
||||
severity: Severity::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn region(content: &str) -> CandidateRegion {
|
||||
CandidateRegion {
|
||||
file: "src/auth.py".into(),
|
||||
start_line: 1,
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn keeps_grounded_and_drops_ungrounded() {
|
||||
let checker = GroundedControlChecker::new(StubJudge {
|
||||
verdict: LlmVerdict {
|
||||
violates: true,
|
||||
snippet: "PASSWORD = \"admin\"".into(),
|
||||
cwe: None,
|
||||
confidence: 0.9,
|
||||
},
|
||||
});
|
||||
let regions = vec![
|
||||
region("x = 1\nPASSWORD = \"admin\"\n"), // quotes real code → grounded
|
||||
region("totally unrelated code\n"), // snippet absent → dropped
|
||||
];
|
||||
let findings = checker.check(&spec(), ®ions, "repo").await;
|
||||
assert_eq!(findings.len(), 1);
|
||||
assert_eq!(findings[0].rule_id.as_deref(), Some("cra-ai-8"));
|
||||
assert_eq!(findings[0].line_number, Some(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn non_violation_yields_nothing() {
|
||||
let checker = GroundedControlChecker::new(StubJudge {
|
||||
verdict: LlmVerdict {
|
||||
violates: false,
|
||||
snippet: String::new(),
|
||||
cwe: None,
|
||||
confidence: 0.0,
|
||||
},
|
||||
});
|
||||
let findings = checker
|
||||
.check(&spec(), &[region("PASSWORD = \"admin\"\n")], "repo")
|
||||
.await;
|
||||
assert!(findings.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@
|
||||
//! [`OscalControlsProvider`], which pulls breakpilot-compliance's OSCAL catalog
|
||||
//! and snapshots it locally.
|
||||
|
||||
mod checker;
|
||||
mod judge;
|
||||
mod oscal_provider;
|
||||
|
||||
pub use checker::GroundedControlChecker;
|
||||
pub use judge::{ControlJudge, LlmControlJudge, PROMPT_VERSION};
|
||||
pub use oscal_provider::OscalControlsProvider;
|
||||
|
||||
Reference in New Issue
Block a user