From 82fbf01bb3d27e04eb3af830bdc203cc02e04814 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 19:28:49 +0000 Subject: [PATCH] =?UTF-8?q?mod:=20Ext=2016=20=E2=80=94=20ActivityPub-shape?= =?UTF-8?q?d=20decision=20export,=20350/350?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mod/decision->activity maps a decision to a moderation verb (remove→Delete, ban→Block, hide/escalate→Flag, keep→no activity) shaped like an AP activity, preserving the precise action. mod/decisions->activities batch-exports dropping keeps. With wire (Ext 14) + fed trust (Phase 4) the federated moderation path is end-to-end: decide → activity/wire → peer → trust-gate → apply. +17 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/mod/activity.sx | 40 +++++++++++++++++ lib/mod/conformance.conf | 2 + lib/mod/scoreboard.json | 9 ++-- lib/mod/scoreboard.md | 3 +- lib/mod/tests/activity.sx | 95 +++++++++++++++++++++++++++++++++++++++ plans/mod-on-sx.md | 13 +++++- 6 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 lib/mod/activity.sx create mode 100644 lib/mod/tests/activity.sx diff --git a/lib/mod/activity.sx b/lib/mod/activity.sx new file mode 100644 index 00000000..19f2ec7f --- /dev/null +++ b/lib/mod/activity.sx @@ -0,0 +1,40 @@ +;; lib/mod/activity.sx — export decisions as ActivityPub-shaped events. +;; +;; The rose-ash platform propagates cross-domain effects as ActivityPub-shaped +;; activities. A moderation decision maps to a moderation verb so the rest of the +;; platform (and federated peers) can act on it: remove→Delete, ban→Block, +;; hide/escalate→Flag, keep→no activity. The precise mod action is preserved in +;; :action so a consumer can disambiguate (e.g. hide vs escalate, both Flag). + +(define + mod/action->verb + (fn + (action) + (cond + ((= action "remove") "Delete") + ((= action "ban") "Block") + ((= action "hide") "Flag") + ((= action "escalate") "Flag") + (true nil)))) + +(define + mod/decision->activity + (fn + (d actor) + (let + ((verb (mod/action->verb (get d :action)))) + (if (nil? verb) nil {:type verb :action (get d :action) :actor actor :summary (str "moderation/" (get d :action) " via " (get d :rule)) :object (get d :report-id) :rule (get d :rule)})))) + +;; map a batch of decisions to activities, dropping the no-op keeps +(define + mod/decisions->activities + (fn + (decisions actor) + (reduce + (fn + (acc d) + (let + ((a (mod/decision->activity d actor))) + (if (nil? a) acc (append acc (list a))))) + (list) + decisions))) diff --git a/lib/mod/conformance.conf b/lib/mod/conformance.conf index 0ed46604..c451c523 100644 --- a/lib/mod/conformance.conf +++ b/lib/mod/conformance.conf @@ -23,6 +23,7 @@ PRELOADS=( lib/mod/temporal.sx lib/mod/sla.sx lib/mod/wire.sx + lib/mod/activity.sx lib/mod/lifecycle.sx lib/mod/audit.sx lib/mod/api.sx @@ -49,4 +50,5 @@ SUITES=( "sla:lib/mod/tests/sla.sx:(mod-sla-tests-run!)" "wire:lib/mod/tests/wire.sx:(mod-wire-tests-run!)" "disjunction:lib/mod/tests/disjunction.sx:(mod-disjunction-tests-run!)" + "activity:lib/mod/tests/activity.sx:(mod-activity-tests-run!)" ) diff --git a/lib/mod/scoreboard.json b/lib/mod/scoreboard.json index f1207f71..214d78b7 100644 --- a/lib/mod/scoreboard.json +++ b/lib/mod/scoreboard.json @@ -1,8 +1,8 @@ { "lang": "mod", - "total_passed": 333, + "total_passed": 350, "total_failed": 0, - "total": 333, + "total": 350, "suites": [ {"name":"decide","passed":31,"failed":0,"total":31}, {"name":"audit","passed":29,"failed":0,"total":29}, @@ -20,7 +20,8 @@ {"name":"temporal","passed":15,"failed":0,"total":15}, {"name":"sla","passed":15,"failed":0,"total":15}, {"name":"wire","passed":16,"failed":0,"total":16}, - {"name":"disjunction","passed":10,"failed":0,"total":10} + {"name":"disjunction","passed":10,"failed":0,"total":10}, + {"name":"activity","passed":17,"failed":0,"total":17} ], - "generated": "2026-06-06T19:22:42+00:00" + "generated": "2026-06-06T19:28:13+00:00" } diff --git a/lib/mod/scoreboard.md b/lib/mod/scoreboard.md index 5700d6dc..16224e3d 100644 --- a/lib/mod/scoreboard.md +++ b/lib/mod/scoreboard.md @@ -1,6 +1,6 @@ # mod scoreboard -**333 / 333 passing** (0 failure(s)). +**350 / 350 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -21,3 +21,4 @@ | sla | 15 | 15 | ok | | wire | 16 | 16 | ok | | disjunction | 10 | 10 | ok | +| activity | 17 | 17 | ok | diff --git a/lib/mod/tests/activity.sx b/lib/mod/tests/activity.sx new file mode 100644 index 00000000..00d2d3b0 --- /dev/null +++ b/lib/mod/tests/activity.sx @@ -0,0 +1,95 @@ +;; lib/mod/tests/activity.sx — Ext 16: ActivityPub-shaped decision export. + +(define mod-ap-count 0) +(define mod-ap-pass 0) +(define mod-ap-fail 0) +(define mod-ap-failures (list)) + +(define + mod-ap-test! + (fn + (name got expected) + (begin + (set! mod-ap-count (+ mod-ap-count 1)) + (if + (= got expected) + (set! mod-ap-pass (+ mod-ap-pass 1)) + (begin + (set! mod-ap-fail (+ mod-ap-fail 1)) + (append! + mod-ap-failures + (str name "\n expected: " expected "\n got: " got))))))) + +;; ── action → AP verb ── + +(mod-ap-test! "remove → Delete" (mod/action->verb "remove") "Delete") +(mod-ap-test! "ban → Block" (mod/action->verb "ban") "Block") +(mod-ap-test! "hide → Flag" (mod/action->verb "hide") "Flag") +(mod-ap-test! "escalate → Flag" (mod/action->verb "escalate") "Flag") +(mod-ap-test! "keep → nil (no activity)" (mod/action->verb "keep") nil) + +;; ── single decision → activity ── + +(define mod-ap-spam (mod/mk-report "r1" "a" "bob" "this is spam")) +(define + mod-ap-dec + (mod/decide-report mod-ap-spam (list mod-ap-spam) mod/default-rules)) +(define mod-ap-act (mod/decision->activity mod-ap-dec "instance.example")) + +(mod-ap-test! "activity type is Flag (hide)" (get mod-ap-act :type) "Flag") +(mod-ap-test! "activity object is report id" (get mod-ap-act :object) "r1") +(mod-ap-test! + "activity actor preserved" + (get mod-ap-act :actor) + "instance.example") +(mod-ap-test! + "activity preserves precise action" + (get mod-ap-act :action) + "hide") +(mod-ap-test! "activity carries rule" (get mod-ap-act :rule) "spam-hide") +(mod-ap-test! + "activity summary" + (get mod-ap-act :summary) + "moderation/hide via spam-hide") + +;; ── keep produces no activity ── + +(define mod-ap-clean (mod/mk-report "r2" "a" "b" "a fine post")) +(define + mod-ap-keep + (mod/decide-report mod-ap-clean (list mod-ap-clean) mod/default-rules)) +(mod-ap-test! + "keep decision → nil activity" + (mod/decision->activity mod-ap-keep "x") + nil) + +;; ── abuse → Delete ── + +(define mod-ap-abuse (mod/mk-report "r3" "a" "b" "harassment here")) +(define + mod-ap-abuse-dec + (mod/decide-report mod-ap-abuse (list mod-ap-abuse) mod/default-rules)) +(mod-ap-test! + "abuse decision → Delete activity" + (get (mod/decision->activity mod-ap-abuse-dec "x") :type) + "Delete") + +;; ── batch export drops keeps ── + +(define mod-ap-decisions (list mod-ap-dec mod-ap-keep mod-ap-abuse-dec)) +(define mod-ap-acts (mod/decisions->activities mod-ap-decisions "inst")) +(mod-ap-test! "batch export drops the keep" (len mod-ap-acts) 2) +(mod-ap-test! + "batch export first is the Flag" + (get (first mod-ap-acts) :type) + "Flag") +(mod-ap-test! + "batch export second is the Delete" + (get (nth mod-ap-acts 1) :type) + "Delete") +(mod-ap-test! + "empty decisions → no activities" + (mod/decisions->activities (list) "inst") + (list)) + +(define mod-activity-tests-run! (fn () {:failures mod-ap-failures :total mod-ap-count :passed mod-ap-pass :failed mod-ap-fail})) diff --git a/plans/mod-on-sx.md b/plans/mod-on-sx.md index 27e10349..363019c3 100644 --- a/plans/mod-on-sx.md +++ b/plans/mod-on-sx.md @@ -16,7 +16,7 @@ federation extension. ## Status (rolling) -`bash lib/mod/conformance.sh` → **333/333** (roadmap + 15 extensions complete) +`bash lib/mod/conformance.sh` → **350/350** (roadmap + 16 extensions complete) ## Ground rules @@ -147,6 +147,12 @@ lib/mod/fed.sx derivation goal-by-goal with `[proved]`/`[unproved]` marks and unification bindings. E.g. `Report rc: escalate (rule: repeated-escalate)` … `[proved] report(rc, B, S), report_count(S, N), N >= 3 {B=ann, N=3, S=dave}`. +- [x] **Ext 16 — ActivityPub-shaped export** (`lib/mod/activity.sx`, +17). + `mod/decision->activity` maps a decision to a moderation verb (remove→Delete, + ban→Block, hide/escalate→Flag, keep→no activity) shaped like an AP activity + ({:type :actor :object :summary}), the precise mod action preserved in :action. + `mod/decisions->activities` batch-exports, dropping keeps — ready for the + platform's AP event bus / federated peers. - [x] **Ext 15 — disjunctive conditions** (`policy.sx` + `tests/disjunction.sx`, +10). `(:any (list c1 c2 …))` compiles to Prolog disjunction `(g1 ; g2 ; …)`, completing the condition boolean algebra (AND via the :when list, `:not`, `:any`). @@ -218,6 +224,11 @@ lib/mod/fed.sx ## Progress log +- **Ext 16 — ActivityPub-shaped export, 350/350** (+17). Bridges mod-sx to the + wider rose-ash platform, which propagates cross-domain effects as AP-shaped + activities. Decisions become Flag/Delete/Block activities (keep = no-op); with + the wire format (Ext 14) and fed trust model (Phase 4) the federated moderation + path is now end-to-end: decide → activity/wire → peer → trust-gate → apply. - **Ext 15 — disjunctive conditions, 333/333** (+10). The condition DSL is now a full boolean algebra: AND (the :when list), `:not` (NAF), `:any` (Prolog `;`). cond->goal recurses, so the combinators nest arbitrarily — `:any` of `:not`s, an