1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum Soundness {
20 MayOver,
23 MustUnder,
26 Exact,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum PrecisionContract {
41 ZeroFalsePositive,
43 RecallDriven,
45 FalseNegativesAccepted,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52pub struct PrimitiveSoundness {
53 pub op_id: &'static str,
55 pub soundness: Soundness,
57 pub sanitizer_filter: bool,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct DynamicPrimitiveSoundness {
67 pub op_id: String,
69 pub soundness: Soundness,
71 pub sanitizer_filter: bool,
74}
75
76impl PrimitiveSoundness {
77 #[must_use]
79 pub const fn new(op_id: &'static str, soundness: Soundness) -> Self {
80 Self {
81 op_id,
82 soundness,
83 sanitizer_filter: false,
84 }
85 }
86
87 #[must_use]
89 pub const fn with_sanitizer_filter(mut self) -> Self {
90 self.sanitizer_filter = true;
91 self
92 }
93}
94
95impl DynamicPrimitiveSoundness {
96 #[must_use]
99 pub fn new(op_id: impl Into<String>, soundness: Soundness) -> Self {
100 Self {
101 op_id: op_id.into(),
102 soundness,
103 sanitizer_filter: false,
104 }
105 }
106
107 #[must_use]
109 pub fn with_sanitizer_filter(mut self) -> Self {
110 self.sanitizer_filter = true;
111 self
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118pub struct SoundnessViolation {
119 pub op_id: &'static str,
121 pub soundness: Soundness,
123 pub contract: PrecisionContract,
125 pub fix: &'static str,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize))]
132pub struct DynamicSoundnessViolation {
133 pub op_id: String,
135 pub soundness: Soundness,
137 pub contract: PrecisionContract,
139 pub fix: &'static str,
141}
142
143impl Soundness {
144 #[must_use]
149 pub const fn join(self, other: Soundness) -> Soundness {
150 match (self, other) {
151 (Soundness::MayOver, _) | (_, Soundness::MayOver) => Soundness::MayOver,
152 (Soundness::MustUnder, Soundness::MustUnder) => Soundness::MustUnder,
153 (Soundness::MustUnder, Soundness::Exact) | (Soundness::Exact, Soundness::MustUnder) => {
154 Soundness::MustUnder
155 }
156 (Soundness::Exact, Soundness::Exact) => Soundness::Exact,
157 }
158 }
159}
160
161pub fn validate_pipeline(
175 contract: PrecisionContract,
176 primitives: &[PrimitiveSoundness],
177) -> Result<Soundness, SoundnessViolation> {
178 let mut joined = Soundness::Exact;
179 for primitive in primitives {
180 validate_primitive(contract, *primitive)?;
181 joined = joined.join(primitive.soundness);
182 }
183 Ok(joined)
184}
185
186pub fn validate_dynamic_pipeline(
197 contract: PrecisionContract,
198 primitives: &[DynamicPrimitiveSoundness],
199) -> Result<Soundness, DynamicSoundnessViolation> {
200 let mut joined = Soundness::Exact;
201 for primitive in primitives {
202 validate_dynamic_primitive(contract, primitive)?;
203 joined = joined.join(primitive.soundness);
204 }
205 Ok(joined)
206}
207
208pub fn validate_primitive(
215 contract: PrecisionContract,
216 primitive: PrimitiveSoundness,
217) -> Result<(), SoundnessViolation> {
218 match violation_fix(contract, primitive.soundness, primitive.sanitizer_filter) {
219 None => Ok(()),
220 Some(fix) => Err(SoundnessViolation {
221 op_id: primitive.op_id,
222 soundness: primitive.soundness,
223 contract,
224 fix,
225 }),
226 }
227}
228
229pub fn validate_dynamic_primitive(
236 contract: PrecisionContract,
237 primitive: &DynamicPrimitiveSoundness,
238) -> Result<(), DynamicSoundnessViolation> {
239 match violation_fix(contract, primitive.soundness, primitive.sanitizer_filter) {
240 None => Ok(()),
241 Some(fix) => Err(DynamicSoundnessViolation {
242 op_id: primitive.op_id.clone(),
243 soundness: primitive.soundness,
244 contract,
245 fix,
246 }),
247 }
248}
249
250fn violation_fix(
251 contract: PrecisionContract,
252 soundness: Soundness,
253 sanitizer_filter: bool,
254) -> Option<&'static str> {
255 match (contract, soundness, sanitizer_filter) {
256 (PrecisionContract::ZeroFalsePositive, Soundness::Exact, _)
257 | (PrecisionContract::ZeroFalsePositive, Soundness::MayOver, true)
258 | (PrecisionContract::RecallDriven, Soundness::Exact | Soundness::MayOver, _)
259 | (PrecisionContract::FalseNegativesAccepted, _, _) => None,
260 (PrecisionContract::ZeroFalsePositive, Soundness::MayOver, false) => {
261 Some("Fix: add an explicit sanitizer filter or use only Exact primitives.")
262 }
263 (PrecisionContract::ZeroFalsePositive, Soundness::MustUnder, _) => {
264 Some("Fix: zero-FP pipelines require Exact primitives or filtered MayOver primitives.")
265 }
266 (PrecisionContract::RecallDriven, Soundness::MustUnder, _) => {
267 Some("Fix: recall-driven pipelines cannot include under-approximating primitives.")
268 }
269 }
270}
271
272pub trait SoundnessTagged {
274 fn soundness(&self) -> Soundness;
276}
277
278#[cfg(test)]
279mod tests {
280 use super::{
281 validate_dynamic_pipeline, DynamicPrimitiveSoundness, PrecisionContract, Soundness,
282 };
283
284 #[test]
285 fn dynamic_pipeline_rejects_zero_false_positive_unfiltered_mayover() {
286 let error = validate_dynamic_pipeline(
287 PrecisionContract::ZeroFalsePositive,
288 &[DynamicPrimitiveSoundness::new(
289 "vyre-libs::security::flows_to",
290 Soundness::MayOver,
291 )],
292 )
293 .expect_err("unfiltered MayOver must not satisfy zero false positive contracts");
294
295 assert_eq!(error.op_id, "vyre-libs::security::flows_to");
296 assert_eq!(error.soundness, Soundness::MayOver);
297 assert_eq!(error.contract, PrecisionContract::ZeroFalsePositive);
298 assert!(error.fix.contains("explicit sanitizer filter"));
299 }
300
301 #[test]
302 fn dynamic_pipeline_accepts_zero_false_positive_filtered_mayover() {
303 let soundness = validate_dynamic_pipeline(
304 PrecisionContract::ZeroFalsePositive,
305 &[DynamicPrimitiveSoundness::new(
306 "vyre-libs::security::flows_to_with_sanitizer",
307 Soundness::MayOver,
308 )
309 .with_sanitizer_filter()],
310 )
311 .expect("filtered MayOver should satisfy zero false positive contracts");
312
313 assert_eq!(soundness, Soundness::MayOver);
314 }
315}