aboutsummaryrefslogtreecommitdiff
path: root/cmds/export.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/export.go')
-rw-r--r--cmds/export.go41
1 files changed, 29 insertions, 12 deletions
diff --git a/cmds/export.go b/cmds/export.go
index 9b153ee..994717d 100644
--- a/cmds/export.go
+++ b/cmds/export.go
@@ -7,8 +7,9 @@
// (see relabel.go), or — for a fluxrec pick whose original was never
// exported — is recorded as its own positive.
// Negatives: seeded random sample of recent read+unstarred entries, topped
-// up to a fixed ratio of the positive pool. Also carries --dump-raw fixture
-// capture.
+// up to a fixed ratio of the positive pool. Rows are stamped with the entry's
+// own changed_at (the read/star moment), not the run clock. Also carries
+// --dump-raw fixture capture.
package cmds
import (
@@ -52,6 +53,10 @@ No single feed may hold more than --neg-feed-cap-pct percent of the
negative target (floored at 50 rows), so a few firehoses can't own the
negative class. Each run also writes a JSON summary to --run-out.
+captured_at is each entry's own changed_at (when it was read or starred),
+not the run clock, so a bulk mark-read exported days later keeps its real
+position in the time-split validation window.
+
A star always wins over the read state that produced a negative: a starred
entry already labeled negative is relabeled to positive in place (identity
kept, fresh captured_at, original copied to LABELS.bak), and a star on a
@@ -116,6 +121,20 @@ func saveExportState(path string, st exportState) error {
return nil
}
+// captureTime is when a label was formed: the entry's own last-changed time —
+// for a read+unstarred entry that is when samsc read it, for a starred one
+// when he starred it — rather than the moment this export run happened to
+// visit it. Export-time stamps made every run look like fresh evidence: a bulk
+// mark-read on Monday, exported on Friday, landed in the newest slice and so
+// dominated the time-split validation window, which is supposed to measure
+// recent behaviour. Falls back to the run clock when the API reports none.
+func captureTime(e miniflux.Entry, now time.Time) time.Time {
+ if e.ChangedAt.IsZero() {
+ return now
+ }
+ return e.ChangedAt.UTC()
+}
+
// exportCounts is what the run reports to stdout.
type exportCounts struct {
seen, appended, idDup, urlDup, recHit, recAdded, relabeled int
@@ -153,10 +172,9 @@ func (c *ExportCommand) runExport(stdout io.Writer) error {
var counts exportCounts
var newRows []core.LabelRow
- // Every star seen this run, so rows already in the store can be flipped
- // to positive after the walk (relabelStars).
- starIDs := make(map[int64]bool)
- starURLs := make(map[string]bool)
+ // Every star seen this run, with its own timestamp, so rows already in
+ // the store can be flipped to positive after the walk (relabelStars).
+ var stars starsByKey
maxChanged := state.ChangedAfter
now := time.Now().UTC()
@@ -173,8 +191,7 @@ func (c *ExportCommand) runExport(stdout io.Writer) error {
if ts := e.ChangedAt.Unix(); ts > maxChanged {
maxChanged = ts
}
- starIDs[e.ID] = true
- starURLs[core.NormalizeURL(e.URL)] = true
+ stars.add(e.ID, e.URL, captureTime(e, now))
// Stars on the recommendations feed are not original entries.
// A star outranks whatever the original's read state said, so
@@ -191,7 +208,7 @@ func (c *ExportCommand) runExport(stdout io.Writer) error {
newRows = append(newRows, core.LabelRow{
EntryID: e.ID, URL: e.URL, Title: e.Title,
FeedID: e.Feed.ID, FeedTitle: e.Feed.Title,
- Label: core.LabelPos, CapturedAt: now,
+ Label: core.LabelPos, CapturedAt: captureTime(e, now),
})
seenIDs[e.ID] = true
seenURLs[nu] = true
@@ -212,7 +229,7 @@ func (c *ExportCommand) runExport(stdout io.Writer) error {
newRows = append(newRows, core.LabelRow{
EntryID: e.ID, URL: e.URL, Title: e.Title,
FeedID: e.Feed.ID, FeedTitle: e.Feed.Title,
- Label: core.LabelPos, CapturedAt: now,
+ Label: core.LabelPos, CapturedAt: captureTime(e, now),
})
seenIDs[e.ID] = true
seenURLs[nu] = true
@@ -229,7 +246,7 @@ func (c *ExportCommand) runExport(stdout io.Writer) error {
// as recent evidence. Rewritten in place, backup first — the rows the
// dedup counters above skipped as duplicates are exactly the ones this
// rescues.
- if flipped := relabelStars(existing, starIDs, starURLs, now); flipped > 0 {
+ if flipped := relabelStars(existing, stars, now); flipped > 0 {
if err := rewriteLabels(c.Labels, c.Labels+".bak", existing); err != nil {
return err
}
@@ -365,7 +382,7 @@ func (c *ExportCommand) exportNegatives(client *miniflux.Client, seenIDs map[int
rows = append(rows, core.LabelRow{
EntryID: e.ID, URL: e.URL, Title: e.Title,
FeedID: e.Feed.ID, FeedTitle: e.Feed.Title,
- Label: core.LabelNeg, CapturedAt: now,
+ Label: core.LabelNeg, CapturedAt: captureTime(e, now),
})
negPerFeed[e.Feed.ID]++
seenIDs[e.ID] = true