; feed/rank — scoring + ranking. Scorers are (activity -> number). Ranking is a
; stable two-pass grade-down: first by :at descending (the tiebreak), then by
; score descending — so ties resolve by recency, then by input order. Fully
; deterministic on ties.
;
; Requires: lib/apl/runtime.sx, lib/feed/normalize.sx, lib/feed/stream.sx.

; --- scorers ----------------------------------------------------------------

; recency: half-life decay. score = 0.5 ^ (age / half-life). at==now -> 1.0.
(define
  feed/recency
  (fn
    (now half-life)
    (fn (a) (expt 0.5 (/ (- now (get a :at)) half-life)))))

; velocity: how many of this actor's activities fall in (at-window, at] —
; a burst of recent activity scores higher.
(define
  feed/velocity
  (fn
    (stream window)
    (fn
      (a)
      (len
        (filter
          (fn
            (b)
            (and
              (equal? (get b :actor) (get a :actor))
              (<= (get b :at) (get a :at))
              (> (get b :at) (- (get a :at) window))))
          (feed/items stream))))))

; engagement: how many activities in the stream touch this activity's :object
(define
  feed/engagement
  (fn
    (stream)
    (fn
      (a)
      (len
        (filter
          (fn (b) (equal? (get b :object) (get a :object)))
          (feed/items stream))))))

; composite: weighted sum. parts = (list (list weight scorer) ...)
(define
  feed/composite
  (fn
    (parts)
    (fn
      (a)
      (reduce
        (fn (acc p) (+ acc (* (first p) ((nth p 1) a))))
        0
        parts))))

; --- ranking ----------------------------------------------------------------

; stable reorder of items by key-fn, descending (grade-down is stable)
(define
  feed/-desc-by
  (fn
    (items key-fn)
    (let
      ((keys (make-array (list (len items)) (map key-fn items))))
      (let
        ((order (get (apl-grade-down keys) :ravel)))
        (map (fn (i) (nth items (- i 1))) order)))))

; rank by score descending; ties -> :at descending -> input order
(define
  feed/rank
  (fn
    (stream score-fn)
    (let
      ((by-at (feed/-desc-by (feed/items stream) feed/at)))
      (feed/stream (feed/-desc-by by-at score-fn)))))

; attach a :score to each activity (for inspection / debugging)
(define
  feed/with-scores
  (fn
    (stream score-fn)
    (feed/stream
      (map (fn (a) (assoc a :score (score-fn a))) (feed/items stream)))))

; top-N ranked timeline
(define
  feed/top
  (fn (stream score-fn n) (feed/take (feed/rank stream score-fn) n)))
