diff --git a/lib/feed/conformance.sh b/lib/feed/conformance.sh index d7263cf1..5897e9b7 100755 --- a/lib/feed/conformance.sh +++ b/lib/feed/conformance.sh @@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then exit 1 fi -SUITES=(basic fanout rank integration content notify) +SUITES=(basic fanout rank integration content notify home) OUT_JSON="lib/feed/scoreboard.json" OUT_MD="lib/feed/scoreboard.md" @@ -39,6 +39,7 @@ run_suite() { (load "lib/feed/fed.sx") (load "lib/feed/content.sx") (load "lib/feed/notify.sx") +(load "lib/feed/home.sx") (epoch 2) (eval "(define feed-test-pass 0)") (eval "(define feed-test-fail 0)") diff --git a/lib/feed/home.sx b/lib/feed/home.sx new file mode 100644 index 00000000..a4b3eb3f --- /dev/null +++ b/lib/feed/home.sx @@ -0,0 +1,23 @@ +; feed/home — the capstone. A user's home timeline is the whole pipeline as one +; line: fan all activities out over the follow graph, take the events landing in +; the viewer's inbox, dedupe cross-posts, apply the viewer's ACL, rank, take N. +; +; Requires: fanout.sx, dedupe.sx, acl.sx (feed/timeline), rank.sx, stream.sx. + +; the activities in a user's inbox, as a stream +(define + feed/inbox-stream + (fn (inbox user) (feed/stream (feed/inbox-activities inbox user)))) + +; fanout ∘ inbox ∘ dedupe ∘ ACL ∘ rank ∘ take +(define + feed/home + (fn + (stream graph viewer permit? score-fn n) + (feed/timeline + (feed/dedupe-activities + (feed/inbox-stream (feed/fanout stream graph) viewer)) + viewer + permit? + score-fn + n))) diff --git a/lib/feed/scoreboard.json b/lib/feed/scoreboard.json index 3d682267..74dcdc97 100644 --- a/lib/feed/scoreboard.json +++ b/lib/feed/scoreboard.json @@ -5,9 +5,10 @@ "rank": {"pass": 24, "fail": 0}, "integration": {"pass": 22, "fail": 0}, "content": {"pass": 15, "fail": 0}, - "notify": {"pass": 8, "fail": 0} + "notify": {"pass": 8, "fail": 0}, + "home": {"pass": 6, "fail": 0} }, - "total_pass": 128, + "total_pass": 134, "total_fail": 0, - "total": 128 + "total": 134 } diff --git a/lib/feed/scoreboard.md b/lib/feed/scoreboard.md index ec92d6f4..0e5120b3 100644 --- a/lib/feed/scoreboard.md +++ b/lib/feed/scoreboard.md @@ -10,4 +10,5 @@ _Generated by `lib/feed/conformance.sh`_ | integration | 22 | 0 | 22 | | content | 15 | 0 | 15 | | notify | 8 | 0 | 8 | -| **Total** | **128** | **0** | **128** | +| home | 6 | 0 | 6 | +| **Total** | **134** | **0** | **134** | diff --git a/lib/feed/tests/home.sx b/lib/feed/tests/home.sx new file mode 100644 index 00000000..0a52c2b8 --- /dev/null +++ b/lib/feed/tests/home.sx @@ -0,0 +1,73 @@ +; Follow-up — feed/home capstone pipeline. (feed-test name got expected) + +; alice follows star and bob (edges: follower followee) +(define + G + (feed/follow-graph (list (list "alice" "star") (list "alice" "bob")))) + +; star posts s1 then s2; bob posts b1; star re-posts s1 (cross-post dup); +; zoe posts z1 (alice does NOT follow zoe) +(define + S + (feed/stream + (list + (feed/activity "star" "post" "s1" 10 (list)) + (feed/activity "star" "post" "s2" 20 (list)) + (feed/activity "bob" "post" "b1" 15 (list)) + (feed/activity "star" "post" "s1" 5 (list)) + (feed/activity "zoe" "post" "z1" 30 (list))))) + +(define rec (feed/recency 100 10)) + +(feed-test + "home count (deduped, followed only)" + (feed/count (feed/home S G "alice" feed/permit-public? rec 10)) + 3) + +(feed-test + "home order by recency" + (map + (fn (a) (get a :object)) + (feed/items (feed/home S G "alice" feed/permit-public? rec 10))) + (list "s2" "b1" "s1")) + +(feed-test + "home excludes unfollowed zoe" + (feed/-elem? + "z1" + (map + (fn (a) (get a :object)) + (feed/items (feed/home S G "alice" feed/permit-public? rec 10)))) + false) + +(feed-test + "home top-2" + (map + (fn (a) (get a :object)) + (feed/items (feed/home S G "alice" feed/permit-public? rec 2))) + (list "s2" "b1")) + +(feed-test + "home dedupes cross-post (one s1)" + (len + (filter + (fn (o) (equal? o "s1")) + (map + (fn (a) (get a :object)) + (feed/items + (feed/home S G "alice" feed/permit-public? rec 10))))) + 1) + +; ACL applied per-viewer in the home pipeline +(define + Sacl + (feed/stream + (list (feed/normalize {:actor "star" :object "pub" :at 20}) (feed/normalize {:actor "star" :object "sec" :visible-to (list "carol") :at 25})))) +(define Gacl (feed/follow-graph (list (list "alice" "star")))) + +(feed-test + "home hides activity alice not permitted" + (map + (fn (a) (get a :object)) + (feed/items (feed/home Sacl Gacl "alice" feed/permit-acl? rec 10))) + (list "pub")) diff --git a/plans/feed-on-sx.md b/plans/feed-on-sx.md index 08620c1a..60fcc098 100644 --- a/plans/feed-on-sx.md +++ b/plans/feed-on-sx.md @@ -14,7 +14,7 @@ APL, ACL visibility filtering via `lib/acl/`, federation via fed-sx. ## Status (rolling) -`bash lib/feed/conformance.sh` → **128/128** (Phases 1–4 + TF-IDF + notifications) +`bash lib/feed/conformance.sh` → **134/134** (Phases 1–4 + TF-IDF + notifications + home) ## Ground rules @@ -144,6 +144,9 @@ are function parameters. Real acl-sx / fed-sx wire in at the call site unchanged - [x] Notification feed (verb-filtered, per-recipient) — `notify.sx`: `feed/notifications`, `feed/notify-verbs`, `feed/notify-digest` (collapses "X, Y liked Z" by (verb,object), sorted-deterministic); 8 tests. (128/128 total.) +- [x] **Capstone** `feed/home` — the whole pipeline as one line: fanout ∘ inbox ∘ + dedupe ∘ ACL ∘ rank ∘ take (`home.sx`); 6 tests incl. per-viewer ACL + cross-post + dedupe. (134/134 total.) (none)