use crate::config::TimelineElaborationConfig;
pub const ELABORATION_SYSTEM: &str = "\
You are reviewing specific timeline issues for elaboration. You are given one \
already-detected issue about a fictional world's timeline. Do not invent missing \
facts. In two or three sentences:\n\
- If it is an orphaned event: explain why this event might be significant given its \
title and date, and suggest concretely how it could be integrated (linked to a \
paragraph, a character, or a place) — or whether it reads as deliberate backstory.\n\
- If it is a fuzzy-precision overlap: explain which scenes or moments most likely \
conflict, and the most natural resolution (refine one event's precision, mark them \
concurrent, or accept it as deliberate).\n\
Be specific and brief. End with one short suggested action.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElaborationBudget {
enabled: bool,
remaining: usize,
pub confirm_above: usize,
}
impl ElaborationBudget {
pub fn from_config(cfg: &TimelineElaborationConfig, have_llm: bool) -> Self {
ElaborationBudget {
enabled: cfg.enabled && have_llm,
remaining: cfg.max_calls_per_run,
confirm_above: cfg.confirm_above_calls,
}
}
pub fn off() -> Self {
ElaborationBudget { enabled: false, remaining: 0, confirm_above: 0 }
}
pub fn is_enabled(&self) -> bool {
self.enabled && self.remaining > 0
}
pub fn remaining(&self) -> usize {
self.remaining
}
pub fn needs_confirmation(&self, n: usize) -> bool {
self.enabled && self.confirm_above > 0 && n > self.confirm_above
}
fn take(&mut self) -> bool {
if self.is_enabled() {
self.remaining -= 1;
true
} else {
false
}
}
}
pub fn orphan_prompt(title: &str, date_label: &str, track: &str, reasons: &[String]) -> String {
format!(
"Issue: orphaned timeline event.\n\
Event: \"{title}\" ({date_label}, \"{track}\" track).\n\
Detected because: {}\n\
Elaborate per the instructions.",
reasons.join(" ")
)
}
pub fn overlap_prompt(
titles: &[String],
window_label: &str,
is_cluster: bool,
reasons: &[String],
) -> String {
let kind = if is_cluster { "cluster overlap" } else { "pairwise overlap" };
format!(
"Issue: fuzzy-precision timeline {kind}.\n\
Events: {}.\n\
Overlap window: {window_label}.\n\
Detected because: {}\n\
Elaborate per the instructions.",
titles.join("; "),
reasons.join(" ")
)
}
pub fn elaborate<F>(prompt: &str, budget: &mut ElaborationBudget, mut complete: F) -> Option<String>
where
F: FnMut(&str) -> Option<String>,
{
if !budget.take() {
return None;
}
complete(prompt).map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(enabled: bool, max: usize, confirm: usize) -> TimelineElaborationConfig {
TimelineElaborationConfig { enabled, max_calls_per_run: max, confirm_above_calls: confirm }
}
#[test]
fn budget_disabled_without_llm() {
let b = ElaborationBudget::from_config(&cfg(true, 20, 10), false);
assert!(!b.is_enabled());
}
#[test]
fn budget_caps_calls() {
let mut b = ElaborationBudget::from_config(&cfg(true, 2, 10), true);
let mut calls = 0;
let mut run = || {
elaborate("p", &mut b, |_| {
calls += 1;
Some("ok".into())
})
};
assert_eq!(run(), Some("ok".into()));
assert_eq!(run(), Some("ok".into()));
assert_eq!(run(), None);
assert_eq!(calls, 2);
}
#[test]
fn disabled_budget_is_pattern_only() {
let mut b = ElaborationBudget::from_config(&cfg(false, 20, 10), true);
let mut invoked = false;
let out = elaborate("p", &mut b, |_| {
invoked = true;
Some("x".into())
});
assert_eq!(out, None);
assert!(!invoked, "completion must not run when elaboration is disabled");
}
#[test]
fn empty_completion_falls_back() {
let mut b = ElaborationBudget::from_config(&cfg(true, 5, 10), true);
assert_eq!(elaborate("p", &mut b, |_| Some(" ".into())), None);
assert_eq!(elaborate("p", &mut b, |_| None), None);
}
#[test]
fn needs_confirmation_past_threshold() {
let b = ElaborationBudget::from_config(&cfg(true, 20, 10), true);
assert!(!b.needs_confirmation(10));
assert!(b.needs_confirmation(11));
}
#[test]
fn prompts_carry_the_essentials() {
let op = orphan_prompt("Coronation", "year 120", "main", &["Orphaned for 92 days.".into()]);
assert!(op.contains("Coronation"));
assert!(op.contains("year 120"));
assert!(op.contains("92 days"));
let fp = overlap_prompt(
&["Training".into(), "Journey".into()],
"summer 87",
false,
&["They share a character.".into()],
);
assert!(fp.contains("Training"));
assert!(fp.contains("pairwise overlap"));
assert!(fp.contains("summer 87"));
}
}