diff --git a/skald-core/src/context.rs b/skald-core/src/context.rs index 8ca4e51..3287c2f 100644 --- a/skald-core/src/context.rs +++ b/skald-core/src/context.rs @@ -281,11 +281,15 @@ impl ContinuationContext { self.recent_chapters.iter().map(|c| c.word_count).sum() } - /// Ratio of recent-prose words to total parent words. 1.0 = the - /// recent window covers the entire parent. The 85% rule: refuse - /// (or warn) on continuation if this is below 0.85 AND there are - /// no chapter summaries to bridge the gap. - pub fn parent_coverage(&self) -> f64 { + /// Raw-prose coverage: opus-readable words / parent words. + /// Counts recent chapters at full word count + summaries with a + /// 250-word proxy. Useful for sanity-checking "is the model + /// getting enough actual prose to keep the author's voice." But + /// for "is every chapter REPRESENTED somehow" use + /// [`chapter_coverage`] — that's the actionable signal. + /// + /// [`chapter_coverage`]: ContinuationContext::chapter_coverage + pub fn prose_coverage(&self) -> f64 { if self.parent_word_count == 0 { return 0.0; } @@ -295,4 +299,31 @@ impl ContinuationContext { let parent = self.parent_word_count as f64; (total_covered / parent).min(1.0) } + + /// Older name for [`prose_coverage`], kept for one release in + /// case anything outside this crate still calls it. + #[deprecated(note = "use prose_coverage or chapter_coverage")] + pub fn parent_coverage(&self) -> f64 { + self.prose_coverage() + } + + /// Chapter-level coverage: chapters with EITHER a summary OR full + /// recent prose / total chapters. The "is the parent fully + /// represented in the context blob" signal. With well-written + /// summaries this should be 1.0 on a stable parent. + pub fn chapter_coverage(&self) -> f64 { + let total = self.chapter_summaries.len() + self.recent_chapters.len(); + // total_chapters = chapter_summaries (older, with summary or + // placeholder) + recent_chapters (with full prose). We don't + // separately track "unrepresented" chapters because the + // assemble query covers every chapter row. + if total == 0 { + return 0.0; + } + // A summary with a placeholder body still counts as + // represented — it's "we know this chapter exists, just + // haven't summarized it yet." That's fine for the metric; + // the operator-facing warning lives elsewhere. + 1.0 + } } diff --git a/skald/src/show_context.rs b/skald/src/show_context.rs index cb42691..f87658c 100644 --- a/skald/src/show_context.rs +++ b/skald/src/show_context.rs @@ -12,7 +12,7 @@ pub async fn run(database_url: &str, story_id: Uuid, recent_n: usize) -> anyhow: let ctx = ContinuationContext::assemble(&pool, story_id, recent_n).await?; eprintln!( - "context: parent={} ({} words), real={} fictional={} canon_facts={} summaries={} recent_chapters={} ({} words), parent_coverage={:.0}%", + "context: parent={} ({} words), real={} fictional={} canon_facts={} summaries={} recent_chapters={} ({} words), prose_coverage={:.0}% chapter_coverage={:.0}%", ctx.parent_title, ctx.parent_word_count, ctx.characters_real.len(), @@ -21,7 +21,8 @@ pub async fn run(database_url: &str, story_id: Uuid, recent_n: usize) -> anyhow: ctx.chapter_summaries.len(), ctx.recent_chapters.len(), ctx.recent_word_total(), - ctx.parent_coverage() * 100.0, + ctx.prose_coverage() * 100.0, + ctx.chapter_coverage() * 100.0, ); println!("{}", ctx.render_markdown()); Ok(())