pub mod check;
pub mod deep;
pub mod grants;
pub mod walk;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct ScenePos {
pub chapter_ord: u32,
pub scene_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KnowledgeItem {
pub topic: String,
pub secret: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrantSource {
Presence,
Declared,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Grant {
pub character: String,
pub topic: String,
pub at: ScenePos,
pub source: GrantSource,
pub anchor: Option<Uuid>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UseVia {
Dialogue,
Pov,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Use {
pub character: String,
pub topic: String,
pub at: ScenePos,
pub via: UseVia,
pub anchor: Uuid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Info,
Notice,
Break,
}
impl Severity {
pub fn rank(self) -> u8 {
match self {
Severity::Info => 0,
Severity::Notice => 1,
Severity::Break => 2,
}
}
pub fn label(self) -> &'static str {
match self {
Severity::Info => "info",
Severity::Notice => "notice",
Severity::Break => "break",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KnowledgeFinding {
pub kind: &'static str,
pub severity: Severity,
pub chapter: u32,
pub anchor: Option<Uuid>,
pub character: String,
pub topic: String,
pub message: String,
}
pub fn earliest_grant<'a>(grants: &'a [Grant], character: &str, topic: &str) -> Option<&'a Grant> {
grants
.iter()
.filter(|g| g.character == character && g.topic == topic)
.min_by_key(|g| g.at)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scene_pos_orders_by_chapter_then_scene() {
let a = ScenePos { chapter_ord: 1, scene_index: 2 };
let b = ScenePos { chapter_ord: 2, scene_index: 0 };
let c = ScenePos { chapter_ord: 1, scene_index: 3 };
assert!(a < b, "earlier chapter is earlier");
assert!(a < c, "same chapter, earlier scene is earlier");
assert!(c < b, "chapter dominates scene");
assert_eq!(a.min(c), a);
}
#[test]
fn severity_rank_orders_break_highest() {
assert!(Severity::Break.rank() > Severity::Notice.rank());
assert!(Severity::Notice.rank() > Severity::Info.rank());
assert_eq!(Severity::Break.label(), "break");
}
#[test]
fn earliest_grant_picks_the_first_matching_and_ignores_others() {
let grants = vec![
Grant {
character: "Mara".into(),
topic: "the betrayal".into(),
at: ScenePos { chapter_ord: 7, scene_index: 0 },
source: GrantSource::Declared,
anchor: None,
},
Grant {
character: "Mara".into(),
topic: "the betrayal".into(),
at: ScenePos { chapter_ord: 4, scene_index: 1 },
source: GrantSource::Presence, anchor: None,
},
Grant {
character: "Bob".into(),
topic: "the betrayal".into(),
at: ScenePos { chapter_ord: 1, scene_index: 0 },
source: GrantSource::Presence, anchor: None,
},
Grant {
character: "Mara".into(),
topic: "the map".into(),
at: ScenePos { chapter_ord: 1, scene_index: 0 },
source: GrantSource::Declared, anchor: None,
},
];
let g = earliest_grant(&grants, "Mara", "the betrayal").expect("Mara knows the betrayal");
assert_eq!(g.at, ScenePos { chapter_ord: 4, scene_index: 1 });
assert_eq!(g.source, GrantSource::Presence);
assert!(earliest_grant(&grants, "Mara", "the heir").is_none(), "never granted → None");
assert!(earliest_grant(&grants, "Sella", "the betrayal").is_none(), "other character → None");
}
}