use std::collections::HashSet;
use uuid::Uuid;
pub(crate) mod intensity;
pub(crate) mod scene_sequel;
pub(crate) mod shape;
pub(crate) mod synthetic;
pub(crate) mod walk;
pub(crate) fn deterministic_findings(
rt: &ReadThrough,
layout: &crate::project::ProjectLayout,
h: &crate::store::hierarchy::Hierarchy,
cfg: &crate::config::Config,
) -> Vec<ReaderFinding> {
let mut f = rt.ranked_findings();
f.extend(scene_sequel::scan(layout, h, cfg));
f.extend(shape::scan(rt, cfg));
rank(&mut f);
dedupe(f)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Severity {
Info,
Notice,
Concern,
}
impl Severity {
pub(crate) fn rank(self) -> u8 {
match self {
Severity::Info => 0,
Severity::Notice => 1,
Severity::Concern => 2,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Notice => "notice",
Severity::Concern => "concern",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ReaderFinding {
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 ReaderFinding {
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(","))
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ChapterRead {
pub chapter: u32,
pub title: String,
pub measured_intensity: Option<f32>,
pub new_entities: Vec<String>,
pub opened_threads: Vec<String>,
pub resolved_threads: Vec<String>,
pub findings: Vec<ReaderFinding>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ReadThrough {
pub chapters: Vec<ChapterRead>,
pub curve: Vec<(f32, f32)>,
}
impl ReadThrough {
pub(crate) fn ranked_findings(&self) -> Vec<ReaderFinding> {
let mut all: Vec<ReaderFinding> =
self.chapters.iter().flat_map(|c| c.findings.iter().cloned()).collect();
rank(&mut all);
dedupe(all)
}
}
pub(crate) fn dedupe(findings: Vec<ReaderFinding>) -> Vec<ReaderFinding> {
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 [ReaderFinding]) {
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]) -> ReaderFinding {
let entities: Vec<String> = entities.iter().map(|e| e.to_string()).collect();
ReaderFinding {
kind,
severity: sev,
chapter,
anchor: None,
dedup_key: ReaderFinding::make_dedup_key(kind, &entities, chapter),
entities,
message: String::new(),
source: "walk",
}
}
#[test]
fn dedup_key_is_order_and_case_insensitive() {
let a = ReaderFinding::make_dedup_key("confusion", &["Mara".into(), "Joren".into()], 3);
let b = ReaderFinding::make_dedup_key("confusion", &["joren".into(), "mara".into()], 3);
assert_eq!(a, b);
assert_ne!(a, ReaderFinding::make_dedup_key("confusion", &["Mara".into()], 3));
assert_ne!(a, ReaderFinding::make_dedup_key("info_dump", &["Mara".into(), "Joren".into()], 3));
}
#[test]
fn dedupe_folds_same_problem() {
let findings = vec![
f("confusion", Severity::Concern, 3, &["Aldous"]),
f("confusion", Severity::Info, 3, &["aldous"]),
f("attention_dip", Severity::Info, 5, &[]),
];
assert_eq!(dedupe(findings).len(), 2);
}
#[test]
fn rank_orders_most_serious_then_chapter() {
let mut findings = vec![
f("attention_dip", Severity::Info, 2, &["a"]),
f("put_down_risk", Severity::Concern, 9, &["b"]),
f("unpaid_setup", Severity::Notice, 1, &["c"]),
];
rank(&mut findings);
assert_eq!(findings[0].severity, Severity::Concern);
assert_eq!(findings[1].severity, Severity::Notice);
assert_eq!(findings[2].severity, Severity::Info);
}
#[test]
fn read_through_ranks_and_dedupes_across_chapters() {
let rt = ReadThrough {
chapters: vec![
ChapterRead {
chapter: 2,
findings: vec![f("attention_dip", Severity::Info, 2, &[])],
..Default::default()
},
ChapterRead {
chapter: 5,
findings: vec![
f("put_down_risk", Severity::Concern, 5, &[]),
f("attention_dip", Severity::Info, 2, &[]),
],
..Default::default()
},
],
curve: Vec::new(),
};
let ranked = rt.ranked_findings();
assert_eq!(ranked.len(), 2, "the duplicate attention_dip is folded");
assert_eq!(ranked[0].kind, "put_down_risk", "most serious first");
}
}