mod: Ext 7 — repeat-offender escalation (audit log as evidence), 223/223
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m13s

mod/subject-sanctions counts prior hide/remove/ban decisions about a subject from
the append-only audit log; mod/decide-escalating upgrades a sanction to :ban when
the subject has >= k priors. Non-sanction outcomes (keep/escalate) pass through.
Closes the loop between audit and policy — the trail feeds future decisions. Own
suite. +19 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:29:36 +00:00
parent 01be84b5d8
commit c3668e4461
6 changed files with 197 additions and 6 deletions

View File

@@ -15,6 +15,7 @@ PRELOADS=(
lib/mod/engine.sx lib/mod/engine.sx
lib/mod/explain.sx lib/mod/explain.sx
lib/mod/severity.sx lib/mod/severity.sx
lib/mod/offenders.sx
lib/mod/lifecycle.sx lib/mod/lifecycle.sx
lib/mod/audit.sx lib/mod/audit.sx
lib/mod/api.sx lib/mod/api.sx
@@ -32,4 +33,5 @@ SUITES=(
"link:lib/mod/tests/link.sx:(mod-link-tests-run!)" "link:lib/mod/tests/link.sx:(mod-link-tests-run!)"
"lint:lib/mod/tests/lint.sx:(mod-lint-tests-run!)" "lint:lib/mod/tests/lint.sx:(mod-lint-tests-run!)"
"severity:lib/mod/tests/severity.sx:(mod-severity-tests-run!)" "severity:lib/mod/tests/severity.sx:(mod-severity-tests-run!)"
"offenders:lib/mod/tests/offenders.sx:(mod-offenders-tests-run!)"
) )

59
lib/mod/offenders.sx Normal file
View File

@@ -0,0 +1,59 @@
;; lib/mod/offenders.sx — repeat-offender escalation (audit log as evidence).
;;
;; The append-only audit trail is itself a source of evidence: a subject already
;; sanctioned several times is a repeat offender. mod/decide-escalating decides a
;; report normally, then — if the action is a sanction and the subject has at
;; least k PRIOR sanctions in the audit log — upgrades it to :ban. This is the one
;; place a decision depends on history beyond the single report, and it reads that
;; history from the audit log rather than re-deriving it.
(define
mod/sanction?
(fn
(action)
(mod/any? (fn (a) (= a action)) (list "hide" "remove" "ban"))))
;; count of prior sanctioning decisions in the audit log about a subject
(define
mod/subject-sanctions
(fn
(subject)
(reduce
(fn
(acc e)
(let
((r (mod/get-report (get e :report-id))))
(if
(nil? r)
acc
(if
(if
(= (mod/report-about r) subject)
(mod/sanction? (get e :action))
false)
(+ acc 1)
acc))))
0
(mod/audit-all))))
(define
mod/repeat-offender?
(fn (subject k) (<= k (mod/subject-sanctions subject))))
(define
mod/decide-escalating
(fn
(id k)
(let
((r (mod/get-report id)))
(if
(nil? r)
nil
(let
((priors (mod/subject-sanctions (mod/report-about r))))
(let
((d (mod/decide id)))
(if
(if (mod/sanction? (get d :action)) (<= k priors) false)
{:action "ban" :proof {:goals (get (get d :proof) :goals) :prior-sanctions priors :evidence (get (get d :proof) :evidence) :conditions (list) :rule "repeat-offender-ban" :count (get (get d :proof) :count)} :report-id id :rule "repeat-offender-ban" :strategy "escalating"}
d)))))))

View File

@@ -1,8 +1,8 @@
{ {
"lang": "mod", "lang": "mod",
"total_passed": 204, "total_passed": 223,
"total_failed": 0, "total_failed": 0,
"total": 204, "total": 223,
"suites": [ "suites": [
{"name":"decide","passed":31,"failed":0,"total":31}, {"name":"decide","passed":31,"failed":0,"total":31},
{"name":"audit","passed":29,"failed":0,"total":29}, {"name":"audit","passed":29,"failed":0,"total":29},
@@ -11,7 +11,8 @@
{"name":"extensions","passed":32,"failed":0,"total":32}, {"name":"extensions","passed":32,"failed":0,"total":32},
{"name":"link","passed":12,"failed":0,"total":12}, {"name":"link","passed":12,"failed":0,"total":12},
{"name":"lint","passed":14,"failed":0,"total":14}, {"name":"lint","passed":14,"failed":0,"total":14},
{"name":"severity","passed":14,"failed":0,"total":14} {"name":"severity","passed":14,"failed":0,"total":14},
{"name":"offenders","passed":19,"failed":0,"total":19}
], ],
"generated": "2026-06-06T18:19:30+00:00" "generated": "2026-06-06T18:29:10+00:00"
} }

View File

@@ -1,6 +1,6 @@
# mod scoreboard # mod scoreboard
**204 / 204 passing** (0 failure(s)). **223 / 223 passing** (0 failure(s)).
| Suite | Passed | Total | Status | | Suite | Passed | Total | Status |
|-------|--------|-------|--------| |-------|--------|-------|--------|
@@ -12,3 +12,4 @@
| link | 12 | 12 | ok | | link | 12 | 12 | ok |
| lint | 14 | 14 | ok | | lint | 14 | 14 | ok |
| severity | 14 | 14 | ok | | severity | 14 | 14 | ok |
| offenders | 19 | 19 | ok |

115
lib/mod/tests/offenders.sx Normal file
View File

@@ -0,0 +1,115 @@
;; lib/mod/tests/offenders.sx — Ext 7: repeat-offender escalation.
(define mod-off-count 0)
(define mod-off-pass 0)
(define mod-off-fail 0)
(define mod-off-failures (list))
(define
mod-off-test!
(fn
(name got expected)
(begin
(set! mod-off-count (+ mod-off-count 1))
(if
(= got expected)
(set! mod-off-pass (+ mod-off-pass 1))
(begin
(set! mod-off-fail (+ mod-off-fail 1))
(append!
mod-off-failures
(str name "\n expected: " expected "\n got: " got)))))))
;; ── sanction? predicate ──
(mod-off-test! "hide is a sanction" (mod/sanction? "hide") true)
(mod-off-test! "remove is a sanction" (mod/sanction? "remove") true)
(mod-off-test! "ban is a sanction" (mod/sanction? "ban") true)
(mod-off-test! "keep is not a sanction" (mod/sanction? "keep") false)
(mod-off-test! "escalate is not a sanction" (mod/sanction? "escalate") false)
;; ── repeat-offender escalation over the audit log ──
(mod/reset!)
(mod/report "u1" "spammer" "this is spam")
(mod/report "u2" "spammer" "buy now offer")
(mod/report "u3" "spammer" "click here free money")
(mod/report "u4" "innocent" "fine post")
(mod-off-test!
"no sanctions before any decision"
(mod/subject-sanctions "spammer")
0)
(define mod-off-d1 (mod/decide-escalating "r1" 2))
(mod-off-test!
"first spam → hide (0 priors)"
(get mod-off-d1 :action)
"hide")
(mod-off-test!
"one sanction recorded"
(mod/subject-sanctions "spammer")
1)
(define mod-off-d2 (mod/decide-escalating "r2" 2))
(mod-off-test!
"second spam → hide (1 prior, below k=2)"
(get mod-off-d2 :action)
"hide")
(mod-off-test!
"two sanctions recorded"
(mod/subject-sanctions "spammer")
2)
(define mod-off-d3 (mod/decide-escalating "r3" 2))
(mod-off-test!
"third spam → ban (2 priors ≥ k)"
(get mod-off-d3 :action)
"ban")
(mod-off-test!
"ban decision names repeat-offender rule"
(get mod-off-d3 :rule)
"repeat-offender-ban")
(mod-off-test!
"ban proof records prior sanction count"
(get (get mod-off-d3 :proof) :prior-sanctions)
2)
;; ── different subjects accumulate independently ──
(define mod-off-d4 (mod/decide-escalating "r4" 2))
(mod-off-test!
"innocent keep → not escalated"
(get mod-off-d4 :action)
"keep")
(mod-off-test!
"innocent has no sanctions"
(mod/subject-sanctions "innocent")
0)
(mod-off-test!
"repeat-offender? true for spammer at k=2"
(mod/repeat-offender? "spammer" 2)
true)
(mod-off-test!
"repeat-offender? false for innocent at k=1"
(mod/repeat-offender? "innocent" 1)
false)
;; ── non-sanction decisions are never upgraded to ban ──
;; r5 is a clean post, but it is the 4th report about "spammer", so the
;; repeated-report rule escalates it. escalate is not a sanction, so it passes
;; through decide-escalating unchanged (never becomes :ban).
(mod/report "u5" "spammer" "a perfectly fine post")
(define mod-off-d5 (mod/decide-escalating "r5" 1))
(mod-off-test!
"non-sanction (escalate) decision is not upgraded to ban"
(get mod-off-d5 :action)
"escalate")
(mod-off-test!
"decide-escalating unknown id → nil"
(mod/decide-escalating "r99" 2)
nil)
(define mod-offenders-tests-run! (fn () {:failures mod-off-failures :total mod-off-count :passed mod-off-pass :failed mod-off-fail}))

View File

@@ -16,7 +16,7 @@ federation extension.
## Status (rolling) ## Status (rolling)
`bash lib/mod/conformance.sh`**204/204** (roadmap + 6 extensions complete) `bash lib/mod/conformance.sh`**223/223** (roadmap + 7 extensions complete)
## Ground rules ## Ground rules
@@ -147,6 +147,13 @@ lib/mod/fed.sx
derivation goal-by-goal with `[proved]`/`[unproved]` marks and unification derivation goal-by-goal with `[proved]`/`[unproved]` marks and unification
bindings. E.g. `Report rc: escalate (rule: repeated-escalate)` … `[proved] 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}`. report(rc, B, S), report_count(S, N), N >= 3 {B=ann, N=3, S=dave}`.
- [x] **Ext 7 — repeat-offender escalation** (`lib/mod/offenders.sx`, +19). The
audit log as evidence: `mod/subject-sanctions` counts prior hide/remove/ban
decisions about a subject; `mod/decide-escalating id k` decides normally then
upgrades a *sanction* to `:ban` when the subject already has ≥k prior sanctions.
Non-sanction outcomes (keep/escalate) pass through untouched. First decision
whose input spans history beyond the single report — read from the trail, not
re-derived.
- [x] **Ext 6 — strictest-wins strategy** (`lib/mod/severity.sx`, +14). Alternative - [x] **Ext 6 — strictest-wins strategy** (`lib/mod/severity.sx`, +14). Alternative
to first-match: `mod/decide-strictest` collects every proven rule (`pl-query-all`) to first-match: `mod/decide-strictest` collects every proven rule (`pl-query-all`)
and picks the highest-`mod/action-severity` action (keep<escalate<hide<remove<ban). and picks the highest-`mod/action-severity` action (keep<escalate<hide<remove<ban).
@@ -165,6 +172,12 @@ lib/mod/fed.sx
## Progress log ## Progress log
- **Ext 7 — repeat-offender escalation, 223/223** (+19). Decisions can now depend
on history: the append-only audit log is read back as evidence, and a subject
with k prior sanctions has its next sanction upgraded to `:ban`. Closes the loop
between audit (Phase 2) and policy — the trail isn't just a record, it feeds
future decisions. Non-sanction outcomes never escalate (verified: a clean post
that the count rule escalates stays `:escalate`, never `:ban`).
- **Ext 6 — strictest-wins strategy, 204/204** (+14). A second decision strategy - **Ext 6 — strictest-wins strategy, 204/204** (+14). A second decision strategy
alongside first-match: collect all proven rules and apply the harshest sanction. alongside first-match: collect all proven rules and apply the harshest sanction.
Shows the substrate supports more than one precedence policy over the same rule Shows the substrate supports more than one precedence policy over the same rule