use std::collections::HashSet;
use uuid::Uuid;
pub(crate) mod coherence;
pub(crate) mod engine;
pub(crate) mod introduce;
pub(crate) mod watch;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Severity {
Info,
Warning,
Contradiction,
}
impl Severity {
pub(crate) fn rank(self) -> u8 {
match self {
Severity::Info => 0,
Severity::Warning => 1,
Severity::Contradiction => 2,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Contradiction => "contradiction",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ContinuityFinding {
pub kind: &'static str,
pub severity: Severity,
pub chapter: u32,
pub anchor: Option<Uuid>,
pub entities: Vec<String>,
pub message: String,
pub source: &'static str,
pub dedup_key: String,
}
impl ContinuityFinding {
pub(crate) fn make_dedup_key(kind: &str, entities: &[String], chapter: u32) -> String {
let mut es: Vec<String> = entities.iter().map(|e| e.to_lowercase()).collect();
es.sort();
format!("{kind}|{}|{chapter}", es.join(","))
}
}
pub(crate) fn dedupe(findings: Vec<ContinuityFinding>) -> Vec<ContinuityFinding> {
let mut seen: HashSet<String> = HashSet::new();
findings.into_iter().filter(|f| seen.insert(f.dedup_key.clone())).collect()
}
pub(crate) fn rank(findings: &mut [ContinuityFinding]) {
findings.sort_by(|a, b| {
b.severity.rank().cmp(&a.severity.rank()).then(a.chapter.cmp(&b.chapter))
});
}
#[cfg(test)]
mod tests {
use super::*;
fn f(kind: &'static str, sev: Severity, chapter: u32, entities: &[&str]) -> ContinuityFinding {
let entities: Vec<String> = entities.iter().map(|e| e.to_string()).collect();
ContinuityFinding {
kind,
severity: sev,
chapter,
anchor: None,
dedup_key: ContinuityFinding::make_dedup_key(kind, &entities, chapter),
entities,
message: String::new(),
source: "test",
}
}
#[test]
fn dedup_key_is_order_and_case_insensitive() {
let a = ContinuityFinding::make_dedup_key("co_location", &["Mara".into(), "Joren".into()], 3);
let b = ContinuityFinding::make_dedup_key("co_location", &["joren".into(), "mara".into()], 3);
assert_eq!(a, b);
assert_ne!(a, ContinuityFinding::make_dedup_key("co_location", &["Mara".into(), "Joren".into()], 4));
assert_ne!(a, ContinuityFinding::make_dedup_key("timeline", &["Mara".into(), "Joren".into()], 3));
}
#[test]
fn dedupe_folds_same_break_from_two_detectors() {
let findings = vec![
f("co_location", Severity::Contradiction, 3, &["Mara", "Joren"]),
f("co_location", Severity::Warning, 3, &["joren", "mara"]),
f("numeric", Severity::Info, 5, &["clock"]),
];
let out = dedupe(findings);
assert_eq!(out.len(), 2);
}
#[test]
fn rank_orders_most_severe_then_chapter() {
let mut findings = vec![
f("numeric", Severity::Info, 2, &["a"]),
f("co_location", Severity::Contradiction, 9, &["b"]),
f("timeline", Severity::Warning, 1, &["c"]),
];
rank(&mut findings);
assert_eq!(findings[0].severity, Severity::Contradiction);
assert_eq!(findings[1].severity, Severity::Warning);
assert_eq!(findings[2].severity, Severity::Info);
}
}