#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IntentKind {
DeliberateAmbiguity,
FramingChoice,
StructuralEcho,
StylisticChoice,
DeliberateTemporalAmbiguity,
DeliberateRepetition,
DeliberateTautology,
VoiceInstabilityIntentional,
ProseBeliefIntentionalDistance,
StylisticPatternDeliberate,
DeliberateVariant,
}
impl IntentKind {
pub fn id(&self) -> &'static str {
match self {
IntentKind::DeliberateAmbiguity => "deliberate_ambiguity",
IntentKind::FramingChoice => "framing_choice",
IntentKind::StructuralEcho => "structural_echo",
IntentKind::StylisticChoice => "stylistic_choice",
IntentKind::DeliberateTemporalAmbiguity => "deliberate_temporal_ambiguity",
IntentKind::DeliberateRepetition => "deliberate_repetition",
IntentKind::DeliberateTautology => "deliberate_tautology",
IntentKind::VoiceInstabilityIntentional => "voice_instability_intentional",
IntentKind::ProseBeliefIntentionalDistance => "prose_belief_intentional_distance",
IntentKind::StylisticPatternDeliberate => "stylistic_pattern_deliberate",
IntentKind::DeliberateVariant => "deliberate_variant",
}
}
pub fn from_id(s: &str) -> Option<IntentKind> {
Some(match s {
"deliberate_ambiguity" => IntentKind::DeliberateAmbiguity,
"framing_choice" => IntentKind::FramingChoice,
"structural_echo" => IntentKind::StructuralEcho,
"stylistic_choice" => IntentKind::StylisticChoice,
"deliberate_temporal_ambiguity" => IntentKind::DeliberateTemporalAmbiguity,
"deliberate_repetition" => IntentKind::DeliberateRepetition,
"deliberate_tautology" => IntentKind::DeliberateTautology,
"voice_instability_intentional" => IntentKind::VoiceInstabilityIntentional,
"prose_belief_intentional_distance" => IntentKind::ProseBeliefIntentionalDistance,
"stylistic_pattern_deliberate" => IntentKind::StylisticPatternDeliberate,
"deliberate_variant" => IntentKind::DeliberateVariant,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeLevel {
Project,
Series,
}
#[derive(Debug, Clone, PartialEq)]
pub enum IntentScope {
Project,
Chapter(String),
ParagraphRange { from: String, to: String },
Character(String),
Scene(String),
TimelineRange { from: String, to: String },
}
impl IntentScope {
pub fn applies_to(&self, ctx: &FindingContext) -> bool {
match self {
IntentScope::Project => true,
IntentScope::Chapter(c) => ctx.chapter_id.as_deref() == Some(c.as_str()),
IntentScope::ParagraphRange { from, to } => {
ctx.paragraph_id.as_deref().is_some_and(|p| in_range(p, from, to))
}
IntentScope::Character(id) => ctx.character_ids.iter().any(|c| c == id),
IntentScope::Scene(s) => ctx.scene.as_deref() == Some(s.as_str()),
IntentScope::TimelineRange { from, to } => {
ctx.timeline_point.as_deref().is_some_and(|t| in_range(t, from, to))
}
}
}
}
fn in_range(value: &str, from: &str, to: &str) -> bool {
let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
value >= lo && value <= hi
}
#[derive(Debug, Clone, Default)]
pub struct FindingContext {
pub paragraph_id: Option<String>,
pub chapter_id: Option<String>,
pub character_ids: Vec<String>,
pub scene: Option<String>,
pub timeline_point: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RawIntentRow {
pub kind: String,
pub description: String,
pub scope: IntentScope,
pub coverage: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConsultationResult {
Emit,
Suppress { entry_id: String, note: String },
}
pub fn consult_raw<'a>(
rows: &'a [RawIntentRow],
coverage_id: &str,
ctx: &FindingContext,
) -> Option<&'a RawIntentRow> {
rows.iter()
.find(|r| r.coverage.iter().any(|c| c == coverage_id) && r.scope.applies_to(ctx))
}
#[cfg(test)]
mod tests {
use super::*;
fn row(coverage: &[&str], scope: IntentScope) -> RawIntentRow {
RawIntentRow {
kind: "deliberate_repetition".into(),
description: "the silence motif is intentional".into(),
scope,
coverage: coverage.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn scope_applies_to_matches_ranges_and_dimensions() {
let ctx = FindingContext {
paragraph_id: Some("ch07-p042".into()),
chapter_id: Some("ch07".into()),
..Default::default()
};
assert!(IntentScope::Project.applies_to(&ctx));
assert!(IntentScope::Chapter("ch07".into()).applies_to(&ctx));
assert!(!IntentScope::Chapter("ch09".into()).applies_to(&ctx));
assert!(IntentScope::ParagraphRange { from: "ch07-p001".into(), to: "ch07-p099".into() }
.applies_to(&ctx));
assert!(!IntentScope::ParagraphRange { from: "ch07-p043".into(), to: "ch07-p099".into() }
.applies_to(&ctx));
}
#[test]
fn consult_raw_finds_covered_in_scope_only() {
let rows = vec![row(&["dictionary_richness"], IntentScope::Chapter("ch07".into()))];
let in_scope = FindingContext { chapter_id: Some("ch07".into()), ..Default::default() };
let out_scope = FindingContext { chapter_id: Some("ch09".into()), ..Default::default() };
assert!(consult_raw(&rows, "dictionary_richness", &in_scope).is_some());
assert!(consult_raw(&rows, "dictionary_richness", &out_scope).is_none());
assert!(consult_raw(&rows, "tautology", &in_scope).is_none());
}
#[test]
fn intent_kind_id_round_trips() {
for k in [IntentKind::DeliberateAmbiguity, IntentKind::DeliberateVariant, IntentKind::StructuralEcho] {
assert_eq!(IntentKind::from_id(k.id()), Some(k));
}
assert!(IntentKind::from_id("nonsense").is_none());
}
}