feed: notification feed — per-recipient inbox, verb filter, (verb,object) digest + 8 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SUITES=(basic fanout rank integration content)
|
SUITES=(basic fanout rank integration content notify)
|
||||||
|
|
||||||
OUT_JSON="lib/feed/scoreboard.json"
|
OUT_JSON="lib/feed/scoreboard.json"
|
||||||
OUT_MD="lib/feed/scoreboard.md"
|
OUT_MD="lib/feed/scoreboard.md"
|
||||||
@@ -38,6 +38,7 @@ run_suite() {
|
|||||||
(load "lib/feed/acl.sx")
|
(load "lib/feed/acl.sx")
|
||||||
(load "lib/feed/fed.sx")
|
(load "lib/feed/fed.sx")
|
||||||
(load "lib/feed/content.sx")
|
(load "lib/feed/content.sx")
|
||||||
|
(load "lib/feed/notify.sx")
|
||||||
(epoch 2)
|
(epoch 2)
|
||||||
(eval "(define feed-test-pass 0)")
|
(eval "(define feed-test-pass 0)")
|
||||||
(eval "(define feed-test-fail 0)")
|
(eval "(define feed-test-fail 0)")
|
||||||
|
|||||||
45
lib/feed/notify.sx
Normal file
45
lib/feed/notify.sx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
; feed/notify — a notification feed is a thin layer over a recipient's inbox:
|
||||||
|
; the events directed at a user, optionally verb-filtered, and a digest that
|
||||||
|
; collapses "alice, bob and 1 other liked X" by (verb, object).
|
||||||
|
;
|
||||||
|
; Requires: lib/feed/normalize.sx, lib/feed/stream.sx, lib/feed/fanout.sx
|
||||||
|
; (feed/inbox-for, feed/-elem?).
|
||||||
|
|
||||||
|
; all inbox events for a user (their raw notifications)
|
||||||
|
(define feed/notifications (fn (inbox user) (feed/inbox-for inbox user)))
|
||||||
|
|
||||||
|
; restrict to notification-worthy verbs (e.g. (list "like" "reply" "follow"))
|
||||||
|
(define
|
||||||
|
feed/notify-verbs
|
||||||
|
(fn
|
||||||
|
(inbox user verbs)
|
||||||
|
(feed/filter
|
||||||
|
(feed/inbox-for inbox user)
|
||||||
|
(fn (ev) (feed/-elem? (get (get ev :activity) :verb) verbs)))))
|
||||||
|
|
||||||
|
; group key "verb|object" — deterministic, sortable
|
||||||
|
(define
|
||||||
|
feed/-notify-key
|
||||||
|
(fn
|
||||||
|
(ev)
|
||||||
|
(let
|
||||||
|
((a (get ev :activity)))
|
||||||
|
(string-append (get a :verb) "|" (get a :object)))))
|
||||||
|
|
||||||
|
; digest: one entry per (verb, object) with the distinct actors and a count,
|
||||||
|
; ordered by key for determinism.
|
||||||
|
(define
|
||||||
|
feed/notify-digest
|
||||||
|
(fn
|
||||||
|
(inbox user)
|
||||||
|
(let
|
||||||
|
((events (feed/items (feed/inbox-for inbox user))))
|
||||||
|
(let
|
||||||
|
((groups (reduce (fn (g ev) (let ((a (get ev :activity)) (k (feed/-notify-key ev))) (let ((cur (get g k {:object (get a :object) :actors (list) :verb (get a :verb)}))) (assoc g k (assoc cur :actors (append (get cur :actors) (list (get a :actor)))))))) {} events)))
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(k)
|
||||||
|
(let
|
||||||
|
((grp (get groups k)))
|
||||||
|
(assoc grp :count (len (get grp :actors)))))
|
||||||
|
(sort (keys groups)))))))
|
||||||
@@ -4,9 +4,10 @@
|
|||||||
"fanout": {"pass": 29, "fail": 0},
|
"fanout": {"pass": 29, "fail": 0},
|
||||||
"rank": {"pass": 24, "fail": 0},
|
"rank": {"pass": 24, "fail": 0},
|
||||||
"integration": {"pass": 22, "fail": 0},
|
"integration": {"pass": 22, "fail": 0},
|
||||||
"content": {"pass": 15, "fail": 0}
|
"content": {"pass": 15, "fail": 0},
|
||||||
|
"notify": {"pass": 8, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 120,
|
"total_pass": 128,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 120
|
"total": 128
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ _Generated by `lib/feed/conformance.sh`_
|
|||||||
| rank | 24 | 0 | 24 |
|
| rank | 24 | 0 | 24 |
|
||||||
| integration | 22 | 0 | 22 |
|
| integration | 22 | 0 | 22 |
|
||||||
| content | 15 | 0 | 15 |
|
| content | 15 | 0 | 15 |
|
||||||
| **Total** | **120** | **0** | **120** |
|
| notify | 8 | 0 | 8 |
|
||||||
|
| **Total** | **128** | **0** | **128** |
|
||||||
|
|||||||
69
lib/feed/tests/notify.sx
Normal file
69
lib/feed/tests/notify.sx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
; Follow-up — notification feed over an inbox. (feed-test name got expected)
|
||||||
|
|
||||||
|
; an inbox is a stream of {:to receiver :activity act} events
|
||||||
|
(define mk-ev (fn (to act) {:activity act :to to}))
|
||||||
|
|
||||||
|
(define
|
||||||
|
IB
|
||||||
|
(feed/stream
|
||||||
|
(list
|
||||||
|
(mk-ev "alice" (feed/activity "bob" "like" "P" 10 (list)))
|
||||||
|
(mk-ev "alice" (feed/activity "carol" "like" "P" 20 (list)))
|
||||||
|
(mk-ev "alice" (feed/activity "dave" "reply" "Q" 30 (list)))
|
||||||
|
(mk-ev "bob" (feed/activity "eve" "like" "R" 40 (list))))))
|
||||||
|
|
||||||
|
; ---------- raw notifications ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"alice notification count"
|
||||||
|
(feed/count (feed/notifications IB "alice"))
|
||||||
|
3)
|
||||||
|
(feed-test
|
||||||
|
"bob notification count"
|
||||||
|
(feed/count (feed/notifications IB "bob"))
|
||||||
|
1)
|
||||||
|
(feed-test
|
||||||
|
"zoe no notifications"
|
||||||
|
(feed/count (feed/notifications IB "zoe"))
|
||||||
|
0)
|
||||||
|
|
||||||
|
; ---------- verb filtering ----------
|
||||||
|
|
||||||
|
(feed-test
|
||||||
|
"alice likes only"
|
||||||
|
(feed/count (feed/notify-verbs IB "alice" (list "like")))
|
||||||
|
2)
|
||||||
|
(feed-test
|
||||||
|
"alice replies only"
|
||||||
|
(feed/count (feed/notify-verbs IB "alice" (list "reply")))
|
||||||
|
1)
|
||||||
|
(feed-test
|
||||||
|
"alice like+reply"
|
||||||
|
(feed/count (feed/notify-verbs IB "alice" (list "like" "reply")))
|
||||||
|
3)
|
||||||
|
(feed-test
|
||||||
|
"alice follow (none)"
|
||||||
|
(feed/count (feed/notify-verbs IB "alice" (list "follow")))
|
||||||
|
0)
|
||||||
|
|
||||||
|
; ---------- digest ----------
|
||||||
|
|
||||||
|
(define dig (feed/notify-digest IB "alice"))
|
||||||
|
|
||||||
|
(feed-test "digest group count" (len dig) 2)
|
||||||
|
(feed-test
|
||||||
|
"digest sorted by key (like|P before reply|Q)"
|
||||||
|
(map (fn (g) (get g :object)) dig)
|
||||||
|
(list "P" "Q"))
|
||||||
|
(feed-test
|
||||||
|
"like group actors"
|
||||||
|
(get (nth dig 0) :actors)
|
||||||
|
(list "bob" "carol"))
|
||||||
|
(feed-test "like group count" (get (nth dig 0) :count) 2)
|
||||||
|
(feed-test "like group verb" (get (nth dig 0) :verb) "like")
|
||||||
|
(feed-test "reply group count" (get (nth dig 1) :count) 1)
|
||||||
|
(feed-test
|
||||||
|
"reply group actors"
|
||||||
|
(get (nth dig 1) :actors)
|
||||||
|
(list "dave"))
|
||||||
|
(feed-test "empty digest for zoe" (feed/notify-digest IB "zoe") (list))
|
||||||
@@ -14,7 +14,7 @@ APL, ACL visibility filtering via `lib/acl/`, federation via fed-sx.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/feed/conformance.sh` → **120/120** (Phases 1–4 + TF-IDF complete)
|
`bash lib/feed/conformance.sh` → **128/128** (Phases 1–4 + TF-IDF + notifications)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -141,7 +141,9 @@ are function parameters. Real acl-sx / fed-sx wire in at the call site unchanged
|
|||||||
- [x] TF-IDF over `:tags` for content ranking — `content.sx`: `feed/tag-df`,
|
- [x] TF-IDF over `:tags` for content ranking — `content.sx`: `feed/tag-df`,
|
||||||
`feed/tag-idf` (log N/df), `feed/tfidf-score`, `feed/by-relevance`; 15 tests.
|
`feed/tag-idf` (log N/df), `feed/tfidf-score`, `feed/by-relevance`; 15 tests.
|
||||||
Composes as a scorer with rank.sx. (120/120 total.)
|
Composes as a scorer with rank.sx. (120/120 total.)
|
||||||
- Notification feed (verb-filtered, per-recipient) as a thin layer over fanout.
|
- [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.)
|
||||||
|
|
||||||
(none)
|
(none)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user