aboutsummaryrefslogtreecommitdiff
path: root/cmds/rss.go
diff options
context:
space:
mode:
authorSam Scholten2026-08-30 20:34:38 +1000
committerSam Scholten2026-08-30 20:34:38 +1000
commiteff960c54620bfba47d40bc3792ba805dea3dab3 (patch)
treea04fd365bb08a5a043b99665f8d7435ba1e989e0 /cmds/rss.go
parent25f47ca899527ed2b16d19209c0546413e057126 (diff)
downloadfluxrec-eff960c54620bfba47d40bc3792ba805dea3dab3.tar.gz
fluxrec-eff960c54620bfba47d40bc3792ba805dea3dab3.zip
rss: provenance + score calibration in items
Item titles gain a rune-capped source-feed suffix so list views (which only ever show the channel title) display provenance inline. Item bodies open with feed, model score, and the batch-wide score median; the median is computed over every scored entry at poll time and stored on the run row as a nil-safe pointer, so old rows render without it. /api/status exposes the median too.
Diffstat (limited to 'cmds/rss.go')
-rw-r--r--cmds/rss.go57
1 files changed, 44 insertions, 13 deletions
diff --git a/cmds/rss.go b/cmds/rss.go
index 3c49513..8679f6f 100644
--- a/cmds/rss.go
+++ b/cmds/rss.go
@@ -8,10 +8,16 @@ import (
"encoding/xml"
"fmt"
"time"
+ "unicode/utf8"
"fluxrec/core"
)
+// Cap on the source-feed name in item titles: feed titles can be
+// arbitrarily long, and the suffix must stay compact so list views keep
+// showing the article title itself.
+const maxFeedTitleRunes = 40
+
const rssXMLHeader = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"
type rssGUID struct {
@@ -40,9 +46,11 @@ type rssDocument struct {
}
// RenderRSS renders runs (oldest to newest, as read from runs.jsonl) as an
-// RSS 2.0 document. The source feed title prefixes each item description so
-// provenance is visible in-reader; item content was already cleaned when
-// the run row was written.
+// RSS 2.0 document. Each item title gets the source feed as a rune-capped
+// suffix (readers show the channel title as the source of every item, so
+// this is the only in-list provenance), and the description body opens
+// with feed + model score + the batch-wide median score. Item content was
+// already cleaned when the run row was written.
func RenderRSS(runs []core.RunRow) ([]byte, error) {
doc := rssDocument{
Version: "2.0",
@@ -54,11 +62,11 @@ func RenderRSS(runs []core.RunRow) ([]byte, error) {
for i := len(runs) - 1; i >= 0; i-- {
for _, k := range runs[i].Kept {
doc.Channel.Items = append(doc.Channel.Items, rssItem{
- Title: k.Title,
+ Title: rssItemTitle(k),
Link: k.URL,
GUID: rssGUID{IsPermaLink: false, Value: fmt.Sprintf("mf:%d", k.EntryID)},
PubDate: k.PublishedAt.Format(time.RFC1123Z),
- Description: rssDescription(k),
+ Description: rssDescription(k, runs[i].ScoreMedian),
})
}
}
@@ -69,14 +77,37 @@ func RenderRSS(runs []core.RunRow) ([]byte, error) {
return append([]byte(rssXMLHeader), body...), nil
}
-// rssDescription builds the item body: source feed name on the first line,
-// then the cleaned content (if any).
-func rssDescription(k core.KeptItem) string {
- if k.Content == "" {
- return k.FeedTitle
+// rssItemTitle builds the displayed title: article title first, then the
+// (rune-capped) source feed as a suffix so narrow list views truncate the
+// provenance, not the title itself.
+func rssItemTitle(k core.KeptItem) string {
+ feed := truncateRunes(k.FeedTitle, maxFeedTitleRunes)
+ if feed == "" {
+ return k.Title
+ }
+ return k.Title + " — " + feed
+}
+
+func truncateRunes(s string, max int) string {
+ if utf8.RuneCountInString(s) <= max {
+ return s
}
- if k.FeedTitle == "" {
- return k.Content
+ return string([]rune(s)[:max]) + "…"
+}
+
+// rssDescription builds the item body: first a headline line with the
+// source feed, model score, and batch-wide median (omitted for run rows
+// written before the median was logged), then the cleaned content (if any).
+func rssDescription(k core.KeptItem, median *float64) string {
+ head := fmt.Sprintf("score %.2f", k.Score)
+ if median != nil {
+ head += fmt.Sprintf(" · batch median %.2f", *median)
+ }
+ if k.FeedTitle != "" {
+ head = k.FeedTitle + " · " + head
+ }
+ if k.Content == "" {
+ return head
}
- return k.FeedTitle + "\n\n" + k.Content
+ return head + "\n\n" + k.Content
}