use serde::{Deserialize, Serialize};
use crate::injection::pattern::PatternResult;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FlaggedWindow {
pub span: [usize; 2],
pub score: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ModelReport {
pub available: bool,
pub score: f64,
#[serde(default)]
pub flagged_windows: Vec<FlaggedWindow>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Verdict {
Accept,
Reject,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScanReport {
pub detectors_run: Vec<String>,
pub pattern: PatternResult,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<ModelReport>,
pub blended_score: f64,
pub reject_threshold: f64,
pub verdict: Verdict,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_report_default_is_unavailable() {
let m = ModelReport::default();
assert!(!m.available);
assert!((m.score - 0.0).abs() < 1e-12);
assert!(m.flagged_windows.is_empty());
}
#[test]
fn scan_report_round_trips_through_json() {
let r = ScanReport {
detectors_run: vec!["pattern".into(), "model".into()],
pattern: PatternResult::default(),
model: Some(ModelReport {
available: true,
score: 0.91,
flagged_windows: vec![FlaggedWindow { span: [0, 512], score: 0.91 }],
}),
blended_score: 0.9,
reject_threshold: 0.85,
verdict: Verdict::Reject,
};
let s = serde_json::to_string(&r).unwrap();
let back: ScanReport = serde_json::from_str(&s).unwrap();
assert_eq!(r, back);
assert!(s.contains("\"verdict\":\"reject\""));
}
#[test]
fn model_omitted_when_none() {
let r = ScanReport {
detectors_run: vec!["pattern".into()],
pattern: PatternResult::default(),
model: None,
blended_score: 0.0,
reject_threshold: 0.85,
verdict: Verdict::Accept,
};
let s = serde_json::to_string(&r).unwrap();
assert!(!s.contains("\"model\""), "model must be omitted when None: {s}");
}
}