diff options
Diffstat (limited to 'cmds/relabel.go')
| -rw-r--r-- | cmds/relabel.go | 77 |
1 files changed, 77 insertions, 0 deletions
diff --git a/cmds/relabel.go b/cmds/relabel.go new file mode 100644 index 0000000..9233daf --- /dev/null +++ b/cmds/relabel.go @@ -0,0 +1,77 @@ +// Star-driven relabeling of the label store. +// +// Negatives are sampled from read+unstarred entries, so "sampled as a +// negative, then starred" is a real trajectory: at capture time the entry was +// unstarred, and later samsc read it and liked it. Without relabeling that +// star changes nothing — the entry is already in the store, so it is deduped +// away by ID or URL — and the row keeps teaching the model that samsc did not +// want the very article he starred. +// +// That is the one feedback channel the recommendations feed creates (a star on +// a fluxrec pick whose original was already labeled), so it has to land as a +// positive rather than be swallowed as a duplicate. +// +// Flipping rewrites labels.jsonl in place: the same deliberate exception +// compact-labels takes (see core/labels.go). The row keeps its identity +// (entry, feed, title) and only its label and captured_at change, so the star +// reads as recent evidence — which is what the time-split validation window +// should see. The original file is copied to a backup before the rewrite. +package cmds + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "time" + + "fluxrec/core" +) + +// relabelStars flips negative rows to positives for entries starred since they +// were labeled. ids and urls are this run's stars: entry IDs as seen on the +// starred entries, and normalized URLs (which is how a star on a rec-feed item +// resolves to the original row). Rows are mutated in place; the count of +// flipped rows is returned. +func relabelStars(rows []core.LabelRow, ids map[int64]bool, urls map[string]bool, now time.Time) int { + flipped := 0 + for i := range rows { + if rows[i].Label != core.LabelNeg { + continue + } + if !ids[rows[i].EntryID] && !urls[core.NormalizeURL(rows[i].URL)] { + continue + } + rows[i].Label = core.LabelPos + rows[i].CapturedAt = now + flipped++ + } + return flipped +} + +// rewriteLabels replaces path with rows, copying the original to backup first +// (backup == "" skips the copy). Same rules as compact-labels' rewrite: the +// backup is written before anything is touched, so an interrupted rewrite +// leaves a recoverable file. +func rewriteLabels(path, backup string, rows []core.LabelRow) error { + orig, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("reading %s for backup: %w", path, err) + } + if backup != "" { + if err := os.WriteFile(backup, orig, 0o644); err != nil { + return fmt.Errorf("writing backup %s: %w", backup, err) + } + } + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + for _, row := range rows { + if err := enc.Encode(row); err != nil { + return fmt.Errorf("encoding label row: %w", err) + } + } + if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil { + return fmt.Errorf("rewriting %s: %w", path, err) + } + return nil +} |
