From 5136249ae56fecc44fde05c9512ce78282183019 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 16:57:05 +0000 Subject: [PATCH] =?UTF-8?q?feed:=20viewer=20mute/block=20=E2=80=94=20mute?= =?UTF-8?q?=20actors/tags/objects=20+=20apply-prefs=20bag=20+=209=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/feed/conformance.sh | 3 +- lib/feed/mute.sx | 44 ++++++++++++++++++++++++++ lib/feed/scoreboard.json | 7 +++-- lib/feed/scoreboard.md | 3 +- lib/feed/tests/mute.sx | 68 ++++++++++++++++++++++++++++++++++++++++ plans/feed-on-sx.md | 5 ++- 6 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 lib/feed/mute.sx create mode 100644 lib/feed/tests/mute.sx diff --git a/lib/feed/conformance.sh b/lib/feed/conformance.sh index 77c1bde5..fa11ef15 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 home dedupe trending) +SUITES=(basic fanout rank integration content notify home dedupe trending mute) OUT_JSON="lib/feed/scoreboard.json" OUT_MD="lib/feed/scoreboard.md" @@ -41,6 +41,7 @@ run_suite() { (load "lib/feed/notify.sx") (load "lib/feed/home.sx") (load "lib/feed/trending.sx") +(load "lib/feed/mute.sx") (epoch 2) (eval "(define feed-test-pass 0)") (eval "(define feed-test-fail 0)") diff --git a/lib/feed/mute.sx b/lib/feed/mute.sx new file mode 100644 index 00000000..1a8c5f59 --- /dev/null +++ b/lib/feed/mute.sx @@ -0,0 +1,44 @@ +; feed/mute — viewer-controlled filtering. ACL (acl.sx) is author-controlled +; visibility; mute is the reader's own preference: hide muted actors or tags. +; Like ACL it is per-viewer and applied per request, never cached. +; +; Requires: lib/feed/normalize.sx, lib/feed/stream.sx, lib/feed/fanout.sx +; (feed/-elem?). + +; drop activities authored by a muted actor +(define + feed/mute-actors + (fn + (stream actors) + (feed/filter + stream + (fn (a) (not (feed/-elem? (get a :actor) actors)))))) + +; drop activities carrying any muted tag +(define + feed/mute-tags + (fn + (stream tags) + (feed/filter + stream + (fn (a) (not (some (fn (t) (feed/-elem? t tags)) (get a :tags))))))) + +; drop activities about a muted object (thread mute) +(define + feed/mute-objects + (fn + (stream objects) + (feed/filter + stream + (fn (a) (not (feed/-elem? (get a :object) objects)))))) + +; apply a viewer preference bag: {:mute-actors (...) :mute-tags (...) :mute-objects (...)} +(define + feed/apply-prefs + (fn + (stream prefs) + (feed/mute-objects + (feed/mute-tags + (feed/mute-actors stream (get prefs :mute-actors (list))) + (get prefs :mute-tags (list))) + (get prefs :mute-objects (list))))) diff --git a/lib/feed/scoreboard.json b/lib/feed/scoreboard.json index 4f6c35af..76cb89ec 100644 --- a/lib/feed/scoreboard.json +++ b/lib/feed/scoreboard.json @@ -8,9 +8,10 @@ "notify": {"pass": 8, "fail": 0}, "home": {"pass": 6, "fail": 0}, "dedupe": {"pass": 9, "fail": 0}, - "trending": {"pass": 11, "fail": 0} + "trending": {"pass": 11, "fail": 0}, + "mute": {"pass": 9, "fail": 0} }, - "total_pass": 154, + "total_pass": 163, "total_fail": 0, - "total": 154 + "total": 163 } diff --git a/lib/feed/scoreboard.md b/lib/feed/scoreboard.md index 29167bad..681af0f8 100644 --- a/lib/feed/scoreboard.md +++ b/lib/feed/scoreboard.md @@ -13,4 +13,5 @@ _Generated by `lib/feed/conformance.sh`_ | home | 6 | 0 | 6 | | dedupe | 9 | 0 | 9 | | trending | 11 | 0 | 11 | -| **Total** | **154** | **0** | **154** | +| mute | 9 | 0 | 9 | +| **Total** | **163** | **0** | **163** | diff --git a/lib/feed/tests/mute.sx b/lib/feed/tests/mute.sx new file mode 100644 index 00000000..81ee1e69 --- /dev/null +++ b/lib/feed/tests/mute.sx @@ -0,0 +1,68 @@ +; Follow-up — viewer mute/block filtering. (feed-test name got expected) + +(define + S + (feed/stream + (list + (feed/normalize {:actor "alice" :object "P1" :at 1 :tags (list "news")}) + (feed/normalize {:actor "bob" :object "P2" :at 2 :tags (list "spam")}) + (feed/normalize {:actor "alice" :object "P3" :at 3 :tags (list "cats")}) + (feed/normalize {:actor "carol" :object "P4" :at 4 :tags (list "news" "spam")})))) + +; ---------- mute actors ---------- + +(feed-test + "mute bob drops his post" + (map + (fn (a) (get a :object)) + (feed/items (feed/mute-actors S (list "bob")))) + (list "P1" "P3" "P4")) +(feed-test + "mute alice drops two" + (feed/count (feed/mute-actors S (list "alice"))) + 2) +(feed-test + "mute nobody keeps all" + (feed/count (feed/mute-actors S (list))) + 4) + +; ---------- mute tags ---------- + +(feed-test + "mute spam tag drops two" + (map + (fn (a) (get a :object)) + (feed/items (feed/mute-tags S (list "spam")))) + (list "P1" "P3")) +(feed-test + "mute news+cats leaves spam-only" + (map + (fn (a) (get a :object)) + (feed/items (feed/mute-tags S (list "news" "cats")))) + (list "P2")) + +; ---------- mute objects ---------- + +(feed-test + "mute object P3 (thread mute)" + (feed/count (feed/mute-objects S (list "P3"))) + 3) + +; ---------- combined prefs ---------- + +(feed-test + "apply-prefs actors + tags" + (map + (fn (a) (get a :object)) + (feed/items (feed/apply-prefs S {:mute-actors (list "bob") :mute-tags (list "cats")}))) + (list "P1" "P4")) +(feed-test + "apply-prefs empty keeps all" + (feed/count (feed/apply-prefs S {})) + 4) +(feed-test + "apply-prefs all three filters" + (map + (fn (a) (get a :object)) + (feed/items (feed/apply-prefs S {:mute-objects (list "P3") :mute-actors (list "carol") :mute-tags (list "spam")}))) + (list "P1")) diff --git a/plans/feed-on-sx.md b/plans/feed-on-sx.md index 7c70ee53..8be2610a 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` → **154/154** (Phases 1–4 + TF-IDF + notifications + home + smart-dedupe + trending) +`bash lib/feed/conformance.sh` → **163/163** (Phases 1–4 + TF-IDF, notifications, home, smart-dedupe, trending, mute) ## Ground rules @@ -154,6 +154,9 @@ are function parameters. Real acl-sx / fed-sx wire in at the call site unchanged - [x] Trending — `feed/trending` / `feed/trending-actors`: objects/actors ranked by activity count in a recency window, count-desc with key-asc tiebreak (`trending.sx`); 11 tests. (154/154 total.) +- [x] Mute/block — `feed/mute-actors` / `feed/mute-tags` / `feed/mute-objects` / + `feed/apply-prefs`: viewer-controlled per-request filtering (complements ACL's + author-controlled visibility) (`mute.sx`); 9 tests. (163/163 total.) (none)