Curation v2 step 1: three-way feedback, rating events, reader profile

- Migration 0002: rating_events, article_embeddings, interest_embeddings,
  article_assessments, candidate_runs, runs.config_json/provider_costs_json,
  issue_articles.why; copies ratings into rating_events and drops ratings and
  feed_priors (scores stays until step 4).
- Vote is Loved | Good | NotForMe; legacy `up` links still verify as Loved.
- Footer offers Loved it / Good / Not for me; the confirmation page offers the
  other two so a mis-tap can be corrected. handle_rating appends one event.
- db::current_ratings implements the latest-explicit-event rule with summaries
  and facets; `ratings list|set|clear` CLI appends source='cli' events.
- data/profile.md replaces the hard-coded reader prose; the system prompt is
  rebuilt every run in the §8.4 order with a recent-verdicts block.
- Weekly rebuild reads summaries, facets and notes; feed priors removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1rCLQeKBgnBo3oTgHuTMe
This commit is contained in:
2026-09-02 03:23:39 +00:00
co-authored by Claude Fable 5.1
parent d0560afa1a
commit 3a9f4b99e0
21 changed files with 1584 additions and 996 deletions
+29
View File
@@ -69,6 +69,8 @@ pub struct Config {
pub out_dir: PathBuf,
/// Scour interests OPML used to seed the taste profile (§3.6).
pub interests_opml: PathBuf,
/// Hand-maintained reader profile loaded for every curation run (§8.2).
pub profile_path: PathBuf,
pub miniflux: MinifluxConfig,
pub deepseek: DeepseekConfig,
@@ -92,6 +94,7 @@ impl Default for Config {
database_path: PathBuf::from("/var/lib/daily-epub/daily-epub.db"),
out_dir: PathBuf::from("/var/lib/daily-epub/out"),
interests_opml: PathBuf::from("data/scour-interests.opml"),
profile_path: PathBuf::from("data/profile.md"),
miniflux: MinifluxConfig::default(),
deepseek: DeepseekConfig::default(),
curation: CurationConfig::default(),
@@ -172,6 +175,7 @@ pub struct CurationConfig {
pub paywall_domains: Vec<String>,
/// The only section names the LLM may use (§3.6 stage B).
pub sections: Vec<String>,
pub feedback: FeedbackConfig,
}
impl Default for CurationConfig {
@@ -193,6 +197,28 @@ impl Default for CurationConfig {
.iter()
.map(|s| s.to_string())
.collect(),
feedback: FeedbackConfig::default(),
}
}
}
/// `[curation.feedback]` — explicit verdict weights and prompt history (§6, §8.4).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct FeedbackConfig {
pub loved_value: f64,
pub good_value: f64,
pub not_for_me_value: f64,
pub verdicts_in_prompt: usize,
}
impl Default for FeedbackConfig {
fn default() -> Self {
Self {
loved_value: 1.0,
good_value: 0.35,
not_for_me_value: -1.0,
verdicts_in_prompt: 60,
}
}
}
@@ -382,6 +408,9 @@ mod tests {
assert_eq!(c.max_daily_usd, 2.0);
assert!(c.world_briefing);
assert_eq!(c.deepseek.model, "deepseek-v4-flash");
assert_eq!(c.profile_path, PathBuf::from("data/profile.md"));
assert_eq!(c.curation.feedback.good_value, 0.35);
assert_eq!(c.curation.feedback.verdicts_in_prompt, 60);
assert_eq!(c.xtc.format, XtcFormat::Xtch);
assert_eq!(c.curation.sections.len(), 8);
c.validate().unwrap();