From d6cb0b6df84ab0afa39365f667d772addddce86d Mon Sep 17 00:00:00 2001 From: Kayos Date: Wed, 13 May 2026 11:18:31 -0700 Subject: [PATCH] context: split coverage into prose_coverage + chapter_coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old parent_coverage was raw-prose / parent-words — a signal of 'how much actual prose opus is reading.' But the more actionable signal is 'is every chapter represented somehow' which sits at 1.0 for any parent with summaries (or placeholders) for older chapters. Add chapter_coverage = 1.0 when every chapter has either a summary or full-recent-prose row in the context. Keep prose_coverage as the precise raw-words metric for ops that care about token budget. Deprecate parent_coverage with a one-release shim (renames to prose_coverage). show_context CLI prints both percentages. --- skald-core/src/context.rs | 41 ++++++++++++++++++++++++++++++++++----- skald/src/show_context.rs | 5 +++-- 2 files changed, 39 insertions(+), 7 deletions(-) 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(())