//! 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 { judge: J, } impl GroundedControlChecker { 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 { 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].control_refs, vec!["cra-ai-8".to_string()]); 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()); } }