diff --git a/lib/mod/conformance.conf b/lib/mod/conformance.conf index fb122cb3..565d3f49 100644 --- a/lib/mod/conformance.conf +++ b/lib/mod/conformance.conf @@ -26,6 +26,7 @@ PRELOADS=( lib/mod/wire.sx lib/mod/activity.sx lib/mod/policies.sx + lib/mod/pipeline.sx lib/mod/lifecycle.sx lib/mod/audit.sx lib/mod/api.sx @@ -55,4 +56,5 @@ SUITES=( "activity:lib/mod/tests/activity.sx:(mod-activity-tests-run!)" "policies:lib/mod/tests/policies.sx:(mod-policies-tests-run!)" "defrule:lib/mod/tests/defrule.sx:(mod-defrule-tests-run!)" + "pipeline:lib/mod/tests/pipeline.sx:(mod-pipeline-tests-run!)" ) diff --git a/lib/mod/pipeline.sx b/lib/mod/pipeline.sx new file mode 100644 index 00000000..c663f531 --- /dev/null +++ b/lib/mod/pipeline.sx @@ -0,0 +1,18 @@ +;; lib/mod/pipeline.sx — end-to-end triage orchestration. +;; +;; A single entry point that runs a report through the subsystem and returns the +;; full artifact bundle: the decision (under the report's domain policy), a +;; human-readable explanation, an ActivityPub-shaped event for the bus, and the +;; wire line for federated peers. Composes policies (Ext 17), explain (Ext 3), +;; activity (Ext 16) and wire (Ext 14) — the modules are independent, this is just +;; the convenience that wires them together for the common "process a report" path. + +(define + mod/triage-pipeline + (fn + (domain r reports actor) + (let ((d (mod/decide-in domain r reports))) {:activity (mod/decision->activity d actor) :action (get d :action) :wire (mod/decision->wire d) :rule (get d :rule) :decision d :explanation (mod/explain d)}))) + +(define mod/pipeline-action (fn (p) (get p :action))) +(define mod/pipeline-activity (fn (p) (get p :activity))) +(define mod/pipeline-wire (fn (p) (get p :wire))) diff --git a/lib/mod/scoreboard.json b/lib/mod/scoreboard.json index f5de3c9c..47f3c4c2 100644 --- a/lib/mod/scoreboard.json +++ b/lib/mod/scoreboard.json @@ -1,8 +1,8 @@ { "lang": "mod", - "total_passed": 375, + "total_passed": 390, "total_failed": 0, - "total": 375, + "total": 390, "suites": [ {"name":"decide","passed":31,"failed":0,"total":31}, {"name":"audit","passed":29,"failed":0,"total":29}, @@ -23,7 +23,8 @@ {"name":"disjunction","passed":10,"failed":0,"total":10}, {"name":"activity","passed":17,"failed":0,"total":17}, {"name":"policies","passed":14,"failed":0,"total":14}, - {"name":"defrule","passed":11,"failed":0,"total":11} + {"name":"defrule","passed":11,"failed":0,"total":11}, + {"name":"pipeline","passed":15,"failed":0,"total":15} ], - "generated": "2026-06-06T19:36:45+00:00" + "generated": "2026-06-06T19:40:03+00:00" } diff --git a/lib/mod/scoreboard.md b/lib/mod/scoreboard.md index 08620704..3b494650 100644 --- a/lib/mod/scoreboard.md +++ b/lib/mod/scoreboard.md @@ -1,6 +1,6 @@ # mod scoreboard -**375 / 375 passing** (0 failure(s)). +**390 / 390 passing** (0 failure(s)). | Suite | Passed | Total | Status | |-------|--------|-------|--------| @@ -24,3 +24,4 @@ | activity | 17 | 17 | ok | | policies | 14 | 14 | ok | | defrule | 11 | 11 | ok | +| pipeline | 15 | 15 | ok | diff --git a/lib/mod/tests/pipeline.sx b/lib/mod/tests/pipeline.sx new file mode 100644 index 00000000..0e52f30b --- /dev/null +++ b/lib/mod/tests/pipeline.sx @@ -0,0 +1,112 @@ +;; lib/mod/tests/pipeline.sx — Ext 19: end-to-end triage orchestration. + +(define mod-pp-count 0) +(define mod-pp-pass 0) +(define mod-pp-fail 0) +(define mod-pp-failures (list)) + +(define + mod-pp-test! + (fn + (name got expected) + (begin + (set! mod-pp-count (+ mod-pp-count 1)) + (if + (= got expected) + (set! mod-pp-pass (+ mod-pp-pass 1)) + (begin + (set! mod-pp-fail (+ mod-pp-fail 1)) + (append! + mod-pp-failures + (str name "\n expected: " expected "\n got: " got))))))) + +(mod/policies-reset!) +(mod/register-policy! + "market" + (mod/ruleset + (mod/defrule "market-spam-remove" :remove (list :classification "spam")) + (mod/defrule "default-keep" :keep))) + +;; ── spam in the market domain: full bundle ── + +(define mod-pp-spam (mod/mk-report "r1" "u" "bob" "this is spam")) +(define + mod-pp + (mod/triage-pipeline "market" mod-pp-spam (list mod-pp-spam) "inst.example")) + +(mod-pp-test! + "pipeline action (market policy → remove)" + (mod/pipeline-action mod-pp) + "remove") +(mod-pp-test! "pipeline rule" (get mod-pp :rule) "market-spam-remove") +(mod-pp-test! + "pipeline explanation mentions the action" + (mod/str-contains? (get mod-pp :explanation) "remove") + true) +(mod-pp-test! + "pipeline activity is Delete (remove)" + (get (mod/pipeline-activity mod-pp) :type) + "Delete") +(mod-pp-test! + "pipeline activity object is the report" + (get (mod/pipeline-activity mod-pp) :object) + "r1") +(mod-pp-test! + "pipeline wire round-trips to the same action" + (get (mod/wire->decision (mod/pipeline-wire mod-pp)) :action) + "remove") + +;; ── same report, blog domain (default) → hide, Flag ── + +(define + mod-pp-blog + (mod/triage-pipeline "blog" mod-pp-spam (list mod-pp-spam) "inst.example")) +(mod-pp-test! + "blog default policy → hide" + (mod/pipeline-action mod-pp-blog) + "hide") +(mod-pp-test! + "blog activity is Flag" + (get (mod/pipeline-activity mod-pp-blog) :type) + "Flag") + +;; ── clean report: keep, no activity, explanation says (none) ── + +(define mod-pp-clean (mod/mk-report "r2" "u" "eve" "a fine post")) +(define + mod-pp-k + (mod/triage-pipeline + "market" + mod-pp-clean + (list mod-pp-clean) + "inst.example")) +(mod-pp-test! "clean → keep" (mod/pipeline-action mod-pp-k) "keep") +(mod-pp-test! "keep → no activity" (mod/pipeline-activity mod-pp-k) nil) +(mod-pp-test! + "keep explanation says no evidence" + (mod/str-contains? (get mod-pp-k :explanation) "Evidence: (none)") + true) +(mod-pp-test! + "keep wire still round-trips" + (get (mod/wire->decision (mod/pipeline-wire mod-pp-k)) :rule) + "default-keep") + +;; ── federated handoff: market decision crosses to a peer, trust-gated ── + +(mod/fed-reset!) +(define mod-pp-peer-dec (mod/wire->decision (mod/pipeline-wire mod-pp))) +(mod-pp-test! + "untrusted peer: market decision is advisory" + (get (mod/fed-receive-decision "peerX" mod-pp-peer-dec) :applied) + false) +(mod/grant-trust "peerY" :mod) +(mod-pp-test! + "trusted peer: market decision applies" + (get (mod/fed-receive-decision "peerY" mod-pp-peer-dec) :applied) + true) +(mod-pp-test! + "applied action is remove" + (get (mod/fed-applied-action "r1") :action) + "remove") + +(define mod-pipeline-tests-run! (fn () {:failures mod-pp-failures :total mod-pp-count :passed mod-pp-pass :failed mod-pp-fail})) diff --git a/plans/mod-on-sx.md b/plans/mod-on-sx.md index af551e59..f613eae3 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` → **375/375** (roadmap + 18 extensions complete) +`bash lib/mod/conformance.sh` → **390/390** (roadmap + 19 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 19 — end-to-end triage pipeline** (`lib/mod/pipeline.sx`, +15). + `mod/triage-pipeline domain r reports actor` runs a report through domain-policy + decision → explanation → AP activity → wire, returning the full bundle. The test + is a genuine integration across 5 modules including a federated handoff (market + decision → wire → peer → trust-gated apply). The capstone that proves the + independently-built modules compose. - [x] **Ext 18 — ergonomic defrule / ruleset** (`lib/mod/defrule.sx`, +11). The roadmap's `(defrule …)` surface, done with `&rest` variadics (no macro needed — conditions are already plain data): `mod/defrule` collects trailing conditions, @@ -234,6 +240,14 @@ lib/mod/fed.sx ## Progress log +- **Ext 19 — end-to-end triage pipeline, 390/390** (+15). Capstone: one + orchestration call composes domain policy + decide + explain + activity + wire, + and the integration test runs the whole federated path (decide in a domain → + wire → peer → trust-gated apply) across 5 modules. Confirms the subsystem — built + module-by-module — actually composes end to end. mod-sx now spans schema → policy + DSL (boolean algebra + count/score/reporters/burst) → engine + proofs → audit → + lifecycle → SLA → federation (trust/wire/AP) → analytics (trace/whatif/lint/batch) + → domain policies → pipeline, all on the green lib/prolog substrate, 390 tests. - **Ext 18 — ergonomic defrule / ruleset, 375/375** (+11). Closes the roadmap's original `defrule` surface. `fn` supports `&rest` here, and conditions evaluate to plain data, so no macro is needed — variadic functions give the ergonomics