use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NovaRule {
pub name: String,
#[serde(default)]
pub meta: BTreeMap<String, String>,
#[serde(default)]
pub keywords: BTreeMap<String, KeywordPattern>,
#[serde(default)]
pub semantics: BTreeMap<String, SemanticPattern>,
#[serde(default)]
pub llm: BTreeMap<String, LlmPattern>,
pub condition: super::condition::ConditionExpr,
}
impl NovaRule {
#[must_use]
pub fn severity_label(&self) -> Option<&str> {
self.meta.get("severity").map(String::as_str)
}
#[must_use]
pub fn category_label(&self) -> Option<&str> {
self.meta.get("category").map(String::as_str)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct KeywordPattern {
pub pattern: String,
pub is_regex: bool,
pub case_sensitive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SemanticPattern {
pub pattern: String,
pub threshold: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LlmPattern {
pub pattern: String,
pub threshold: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NovaMatch {
pub rule_name: String,
pub matched: bool,
pub keyword_hits: BTreeMap<String, bool>,
pub semantic_hits: BTreeMap<String, bool>,
pub llm_hits: BTreeMap<String, bool>,
pub skipped_capabilities: Vec<SkippedCapability>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SkippedCapability {
Semantics,
Llm,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_accessors_return_none_for_missing_keys() {
let rule = NovaRule {
name: "t".into(),
meta: BTreeMap::new(),
keywords: BTreeMap::new(),
semantics: BTreeMap::new(),
llm: BTreeMap::new(),
condition: super::super::condition::ConditionExpr::Literal(true),
};
assert!(rule.severity_label().is_none());
assert!(rule.category_label().is_none());
}
#[test]
fn meta_accessors_round_trip_present_keys() {
let mut meta = BTreeMap::new();
meta.insert("severity".into(), "high".into());
meta.insert("category".into(), "prompt_manipulation/jailbreak".into());
let rule = NovaRule {
name: "t".into(),
meta,
keywords: BTreeMap::new(),
semantics: BTreeMap::new(),
llm: BTreeMap::new(),
condition: super::super::condition::ConditionExpr::Literal(true),
};
assert_eq!(rule.severity_label(), Some("high"));
assert_eq!(rule.category_label(), Some("prompt_manipulation/jailbreak"));
}
}