parlov_analysis/aggregation/coverage_gate.rs
1//! Coverage gate for the `NotPresent` verdict.
2//!
3//! Posterior alone is insufficient evidence. Even when the Bayesian aggregate falls
4//! below the `NotPresent` threshold, the verdict should only be issued when the
5//! endpoint has been meaningfully tested — multiple contradictory techniques across
6//! enough independent strategies, with at least one technique that actually probes
7//! the oracle-relevant boundary.
8
9use crate::aggregation::reducer::{EvidenceEvent, EvidencePolarity};
10
11/// Minimum number of distinct Contradictory technique firings required to claim `NotPresent`.
12pub const MIN_CONTRADICTORY_TECHNIQUES: usize = 3;
13
14/// Minimum weight for a Contradictory event to count as a "Strong" technique. Matches the
15/// Strong bucket from the bucketed-prior calibration: `low_privilege` / `auth_strip` /
16/// `scope_manipulation` all fire at 0.25.
17pub const STRONG_THRESHOLD: f64 = 0.20;
18
19/// Returns `true` iff the events satisfy the gate for `NotPresent`.
20///
21/// Three requirements:
22/// 1. No `Positive` events. Any positive signal — at any weight — disqualifies `NotPresent`.
23/// 2. At least `MIN_CONTRADICTORY_TECHNIQUES` distinct contradictory events fired.
24/// 3. At least one contradictory event has `weight >= STRONG_THRESHOLD`.
25#[must_use]
26pub fn passes_not_present_gate(events: &[EvidenceEvent]) -> bool {
27 let mut has_positive = false;
28 let mut contradictory_count = 0usize;
29 let mut has_strong = false;
30
31 for event in events {
32 match event.polarity {
33 EvidencePolarity::Positive => {
34 has_positive = true;
35 }
36 EvidencePolarity::Contradictory => {
37 contradictory_count += 1;
38 if event.weight >= STRONG_THRESHOLD {
39 has_strong = true;
40 }
41 }
42 }
43 }
44
45 !has_positive && contradictory_count >= MIN_CONTRADICTORY_TECHNIQUES && has_strong
46}
47
48#[cfg(test)]
49#[path = "coverage_gate_tests.rs"]
50mod tests;