diff options
Diffstat (limited to 'cmds/export.go')
| -rw-r--r-- | cmds/export.go | 41 |
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 |
