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
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:
@@ -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
59
lib/mod/offenders.sx
Normal 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)))))))
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
115
lib/mod/tests/offenders.sx
Normal 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}))
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user