Make the interests table the only source of standing interests (step 3)

The OPML file and the profile's ## Interests section become one-time
import inputs; the prompt groups by the stored category and the OPML
config key is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9PrjtUS16PAQve8D4bHgc
This commit is contained in:
2026-09-13 05:20:16 +00:00
co-authored by Claude Fable 5.1
parent f0c0927ab8
commit a78e44f56e
14 changed files with 190 additions and 237 deletions
+4 -1
View File
@@ -35,7 +35,6 @@ fn config_check_prints_the_facts_and_exits_zero_without_keys() {
"config: ",
"database_path: /var/lib/daily-epub/daily-epub.db",
"profile_path: data/profile.md",
"interests_opml: data/scour-interests.opml",
"llm.bulk: deepseek · openai · deepseek-v4-flash",
"key MISSING (set DAILY_EPUB_PROVIDERS__DEEPSEEK__API_KEY)",
"llm.editor: anthropic · anthropic · claude-opus-5 · effort high · max_daily_usd $3.00",
@@ -66,4 +65,8 @@ fn config_check_exits_non_zero_on_an_invalid_config() {
let (code, _, stderr) = run("[deepseek]\nmodel = \"x\"\n");
assert_ne!(code, 0);
assert!(stderr.contains("[providers.deepseek]"), "{stderr}");
let (code, _, stderr) = run("interests_opml = \"data/scour-interests.opml\"\n");
assert_ne!(code, 0);
assert!(stderr.contains("interests_opml"), "{stderr}");
}
+10 -7
View File
@@ -21,6 +21,7 @@ use daily_epub::curate::assess::parse_deep_response;
use daily_epub::curate::editor::parse_selection_response;
use daily_epub::curate::editorial::BriefResponse;
use daily_epub::curate::profile;
use daily_epub::interests;
fn repo(rel: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)
@@ -183,13 +184,14 @@ fn stage_c_fixture_parses_into_the_brief() {
assert!(value.get("section_intros").is_none());
}
/// The taste profile is seeded from this file; a broken export would silently
/// gut the system prompt (§3.6a).
/// The importer relies on this file; a broken export would silently lose
/// standing interests (§3.6a).
#[test]
fn scour_opml_still_yields_the_interest_list() {
let interests = profile::parse_interests(&repo("data/scour-interests.opml"))
.expect("the shipped OPML must parse");
let unique: BTreeSet<String> = interests.iter().map(|n| n.to_lowercase()).collect();
let raw = std::fs::read_to_string(repo("data/scour-interests.opml"))
.expect("the shipped OPML must be readable");
let names = interests::parse_opml(&raw);
let unique: BTreeSet<String> = names.iter().map(|n| n.to_lowercase()).collect();
assert!(
unique.len() > 180,
@@ -206,7 +208,7 @@ fn scour_opml_still_yields_the_interest_list() {
assert!(unique.contains(expected), "{expected} disappeared");
}
assert!(
!interests.iter().any(|n| n.contains("token=")),
!names.iter().any(|n| n.contains("token=")),
"interest names must not leak the Scour token"
);
@@ -217,9 +219,10 @@ fn scour_opml_still_yields_the_interest_list() {
"/data/profile.md"
)))
.expect("profile file");
let grouped = vec![("Imported interests".to_string(), names)];
let document = profile::build(
&profile_file.body,
&interests,
&grouped,
profile::NO_LEARNED_ADJUSTMENTS,
&[],
60,