; Phase 3 — aggregation + ranking. (feed-test name got expected) ; ---------- aggregation ---------- (define A (feed/stream (list (feed/activity "alice" "post" "p1" 5 (list)) (feed/activity "alice" "post" "p2" 15 (list)) (feed/activity "bob" "post" "p3" 25 (list)) (feed/activity "alice" "like" "p1" 35 (list))))) (feed-test "actor-counts" (feed/actor-counts A) {:alice 3 :bob 1}) (feed-test "object-counts" (feed/object-counts A) {:p2 1 :p3 1 :p1 2}) (feed-test "group-by actor alice len" (len (get (feed/group-by A feed/actor) "alice")) 3) (feed-test "group-count empty" (feed/group-count feed/empty feed/actor) {}) ; day bucketing (define D (feed/stream (list (feed/activity "alice" "post" "p1" 5 (list)) (feed/activity "alice" "post" "p2" 8 (list)) (feed/activity "alice" "post" "p3" 12 (list))))) (feed-test "feed/day floor" (feed/day 12 10) 1) (feed-test "feed/day same bucket" (feed/day 8 10) 0) (feed-test "by-actor-day" (feed/by-actor-day D 10) {:alice#0 2 :alice#1 1}) ; ---------- recency ---------- (define rec (feed/recency 100 10)) (feed-test "recency at=now -> 1" (rec (feed/activity "x" "post" "o" 100 (list))) 1) (feed-test "recency age=hl -> .5" (rec (feed/activity "x" "post" "o" 90 (list))) 0.5) (feed-test "recency age=2hl -> .25" (rec (feed/activity "x" "post" "o" 80 (list))) 0.25) ; ---------- velocity ---------- (define vel (feed/velocity D 10)) (feed-test "velocity burst (at=12)" (vel (feed/activity "alice" "post" "z" 12 (list))) 3) (feed-test "velocity mid (at=8)" (vel (feed/activity "alice" "post" "z" 8 (list))) 2) (feed-test "velocity first (at=5)" (vel (feed/activity "alice" "post" "z" 5 (list))) 1) (feed-test "velocity other actor" (vel (feed/activity "bob" "post" "z" 12 (list))) 0) ; ---------- engagement ---------- (define eng (feed/engagement A)) (feed-test "engagement p1" (eng (feed/activity "x" "post" "p1" 0 (list))) 2) (feed-test "engagement p2" (eng (feed/activity "x" "post" "p2" 0 (list))) 1) ; ---------- composite ---------- (define cmp1 (feed/composite (list (list 2 (fn (a) (get a :at)))))) (feed-test "composite single part" (cmp1 (feed/activity "x" "post" "o" 5 (list))) 10) (define cmp2 (feed/composite (list (list 2 (fn (a) (get a :at))) (list 3 (fn (a) 1))))) (feed-test "composite two parts" (cmp2 (feed/activity "x" "post" "o" 5 (list))) 13) ; ---------- ranking ---------- (define R (feed/stream (list (feed/activity "u" "post" "oC" 80 (list)) (feed/activity "u" "post" "oA" 100 (list)) (feed/activity "u" "post" "oB" 90 (list))))) (feed-test "rank by recency objects" (map (fn (a) (get a :object)) (feed/items (feed/rank R rec))) (list "oA" "oB" "oC")) (feed-test "top-2 by recency" (map (fn (a) (get a :object)) (feed/items (feed/top R rec 2))) (list "oA" "oB")) (feed-test "top-2 count" (feed/count (feed/top R rec 2)) 2) ; constant score -> tiebreak by :at descending (define T (feed/stream (list (feed/activity "u" "post" "f" 10 (list)) (feed/activity "u" "post" "g" 30 (list)) (feed/activity "u" "post" "h" 20 (list))))) (feed-test "tiebreak at-desc" (map (fn (a) (get a :object)) (feed/items (feed/rank T (fn (a) 0)))) (list "g" "h" "f")) ; equal score AND equal :at -> stable input order (define E (feed/stream (list (feed/activity "u" "post" "first" 50 (list)) (feed/activity "u" "post" "second" 50 (list))))) (feed-test "stable equal-key input order" (map (fn (a) (get a :object)) (feed/items (feed/rank E (fn (a) 0)))) (list "first" "second")) (feed-test "with-scores attaches score" (get (nth (feed/items (feed/with-scores R rec)) 1) :score) 1) (feed-test "rank preserves count" (feed/count (feed/rank A rec)) 4)