use super::lang::{self, Lang};
use super::types::{CritSeverity, FuzzyOverlapFinding, OrphanFinding, Significance, Staleness, Suspicion};
use crate::pane::output::{kinds, Lifetime, Message, Severity};
pub const PROVENANCE: &str = "timeline-critique";
fn map_severity(s: CritSeverity) -> Severity {
match s {
CritSeverity::Info => Severity::Info,
CritSeverity::Warning => Severity::Warning,
CritSeverity::Contradiction => Severity::Contradiction,
}
}
fn significance_str(s: Significance) -> &'static str {
match s {
Significance::Low => "low",
Significance::Moderate => "moderate",
Significance::High => "high",
}
}
fn staleness_str(s: Staleness) -> &'static str {
match s {
Staleness::Recent => "recent",
Staleness::Old => "old",
}
}
fn suspicion_str(s: Suspicion) -> &'static str {
match s {
Suspicion::Low => "low",
Suspicion::Moderate => "moderate",
Suspicion::High => "high",
Suspicion::Cluster => "cluster",
}
}
pub fn emit_orphan(f: &OrphanFinding, date_label: &str, elaboration: Option<&str>, lang: Lang) {
let reasons = lang::localize_orphan(f, lang);
let mut headline = format!(
"\"{}\" ({}, \"{}\" track) — {}",
f.title,
date_label,
f.track,
reasons.join(" ")
);
let body_en = format!(
"\"{}\" ({}, \"{}\" track) — {}",
f.title,
date_label,
f.track,
f.reasons.join(" ")
);
if let Some(extra) = elaboration {
headline.push(' ');
headline.push_str(extra);
}
let meta = serde_json::json!({
"text": headline,
"body_en": body_en,
"category": "orphan",
"provenance": PROVENANCE,
"timeline": true,
"event_ids": [f.event_id.to_string()],
"severity_label": f.severity.label(),
"significance": significance_str(f.significance),
"staleness": staleness_str(f.staleness),
"age_days": f.age_days,
"track": f.track,
"reasons": f.reasons,
"elaboration": elaboration,
});
let msg = Message::new(
kinds::TIMELINE_ORPHAN_WARNING,
map_severity(f.severity),
Lifetime::UntilActedOn,
meta,
);
crate::pane::output::emit(&msg);
}
pub fn emit_overlap(
f: &FuzzyOverlapFinding,
window_label: &str,
char_names: &[String],
place_names: &[String],
elaboration: Option<&str>,
lang: Lang,
) {
let titles = f
.titles
.iter()
.map(|t| format!("\"{t}\""))
.collect::<Vec<_>>()
.join(" + ");
let line = |reasons: &[String]| {
if f.is_cluster {
format!(
"Cluster of {} fuzzy events overlapping {}: {} — {}",
f.total_events,
window_label,
titles,
reasons.join(" ")
)
} else {
format!("{} overlap {} — {}", titles, window_label, reasons.join(" "))
}
};
let mut headline = line(&lang::overlap_reasons(f, lang));
let body_en = line(&f.reasons);
if let Some(extra) = elaboration {
headline.push(' ');
headline.push_str(extra);
}
let meta = serde_json::json!({
"text": headline,
"body_en": body_en,
"category": "fuzzy_overlap",
"provenance": PROVENANCE,
"timeline": true,
"event_ids": f.event_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
"severity_label": f.severity.label(),
"suspicion": suspicion_str(f.suspicion),
"is_cluster": f.is_cluster,
"track": f.track,
"shared_characters": char_names,
"shared_places": place_names,
"reasons": f.reasons,
"elaboration": elaboration,
});
let msg = Message::new(
kinds::TIMELINE_FUZZY_OVERLAP_WARNING,
map_severity(f.severity),
Lifetime::UntilActedOn,
meta,
);
crate::pane::output::emit(&msg);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::timeline::Precision;
use uuid::Uuid;
#[test]
fn orphan_headline_carries_title_track_and_reason() {
let f = OrphanFinding {
event_id: Uuid::nil(),
title: "The Coronation of Hadrin III".into(),
track: "main".into(),
start_ticks: 0,
precision: Precision::Day,
significance: Significance::High,
staleness: Staleness::Old,
age_days: Some(92),
severity: CritSeverity::Contradiction,
reasons: vec!["Orphaned for 92 days.".into()],
};
let headline = format!(
"\"{}\" ({}, \"{}\" track) — {}",
f.title, "year 120", f.track, f.reasons.join(" ")
);
assert!(headline.contains("Coronation"));
assert!(headline.contains("\"main\" track"));
assert!(headline.contains("92 days"));
assert_eq!(map_severity(f.severity), Severity::Contradiction);
}
#[test]
fn severity_mapping_is_total() {
assert_eq!(map_severity(CritSeverity::Info), Severity::Info);
assert_eq!(map_severity(CritSeverity::Warning), Severity::Warning);
assert_eq!(map_severity(CritSeverity::Contradiction), Severity::Contradiction);
}
}