Reuse an existing category's spelling in the categorizer; document the Interests page (step 9)

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:45:53 +00:00
co-authored by Claude Fable 5.1
parent 055b1df769
commit af70da1493
2 changed files with 17 additions and 7 deletions
+5 -1
View File
@@ -214,7 +214,10 @@ sees complete issues and article chapters and can download available formats
from a single download menu. An
`admin` can additionally follow direct dashboard links from issue entries and
article chapters, rate articles, and use every `/dashboard/*` page, including
settings and jobs. Personalization is shared across accounts for now.
settings and jobs. `/dashboard/interests` lists the standing interests with
their LLM-assigned category and a rating-derived weight, adds or deletes them,
and links every interest name in the UI to the articles it matched.
Personalization is shared across accounts for now.
Visitors can request an account with their preferred username at
`/request-access`; admins review open requests on `/dashboard/users`. Approving
@@ -250,6 +253,7 @@ Set `server.jobs_enabled = false` to make starts unavailable.
| `backfill-social` | Refresh recent social scores. |
| `features-prune` | Remove stale embeddings and curation telemetry. |
| `import-ratings` | Fetch, embed, and rate URLs queued from the Ratings page. |
| `interests-categorize` | File uncategorized interests under categories with the bulk model. |
The Ratings page accepts up to 500 historical article URLs at a time with one
verdict and optional note. Imports run in the background and show per-URL
+11 -5
View File
@@ -354,8 +354,8 @@ async fn categorize_with_llm(db: &Db, pending: &[Interest], llm: &LlmClient) ->
.collect::<HashMap<_, _>>();
let existing_categories = categories
.iter()
.map(|category| category.to_lowercase())
.collect::<BTreeSet<_>>();
.map(|category| (category.to_lowercase(), *category))
.collect::<HashMap<_, _>>();
let mut assigned = BTreeSet::new();
let mut new_categories = BTreeSet::new();
let now = Timestamp::now();
@@ -367,10 +367,16 @@ async fn categorize_with_llm(db: &Db, pending: &[Interest], llm: &LlmClient) ->
if !(1..=60).contains(&category.chars().count()) || !assigned.insert(interest.id) {
continue;
}
set_category(db, interest.id, Some(category), now).await?;
if !existing_categories.contains(&category.to_lowercase()) {
// A model that answers "software" for an existing "Software" must not
// split the category.
let category = match existing_categories.get(&category.to_lowercase()) {
Some(existing) => existing,
None => {
new_categories.insert(category.to_string());
category
}
};
set_category(db, interest.id, Some(category), now).await?;
}
let mut message = format!(
@@ -750,7 +756,7 @@ mod tests {
let backend = Arc::new(MockBackend::new());
backend.push(
r#"{"assignments":[
{"interest":"RUST MACROS","category":" Software "},
{"interest":"RUST MACROS","category":" software "},
{"interest":"Wheel-thrown pottery","category":"Creative crafts"},
{"interest":"Not in the batch","category":"Made up"}
]}"#,