mod: Ext 8 — quorum over distinct reporters (anti-brigade), 232/232
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 55s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 55s
(:reporters-at-least N) compiles to setof(Br, report(_, Br, Sr), Bsr), length(Bsr, Nr), Nr >= N — counts distinct reporters, not raw reports. mod/decide-quorum asserts every report's report/3 fact (base engine scopes to the decided report) so Prolog can aggregate reporters. One user filing 3 reports stays :keep under quorum while the count rule escalates. Own suite. +9 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ PRELOADS=(
|
|||||||
lib/mod/explain.sx
|
lib/mod/explain.sx
|
||||||
lib/mod/severity.sx
|
lib/mod/severity.sx
|
||||||
lib/mod/offenders.sx
|
lib/mod/offenders.sx
|
||||||
|
lib/mod/quorum.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
|
||||||
@@ -34,4 +35,5 @@ SUITES=(
|
|||||||
"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!)"
|
"offenders:lib/mod/tests/offenders.sx:(mod-offenders-tests-run!)"
|
||||||
|
"quorum:lib/mod/tests/quorum.sx:(mod-quorum-tests-run!)"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,12 +40,15 @@
|
|||||||
|
|
||||||
;; ── condition → Prolog goal ──
|
;; ── condition → Prolog goal ──
|
||||||
;;
|
;;
|
||||||
;; (:classification "spam") → classification(Id, spam)
|
;; (:classification "spam") → classification(Id, spam)
|
||||||
;; (:evidence "kind") → evidence(Id, 'kind', _)
|
;; (:evidence "kind") → evidence(Id, 'kind', _)
|
||||||
;; (:attr "verified") → attr(Id, verified)
|
;; (:attr "verified") → attr(Id, verified)
|
||||||
;; (:not <cond>) → not(<cond>) (negation as failure)
|
;; (:not <cond>) → not(<cond>) (negation as failure)
|
||||||
;; (:count-at-least 3) → report(Id, B, S), report_count(S, N), N >= 3
|
;; (:count-at-least 3) → report(Id, B, S), report_count(S, N), N >= 3
|
||||||
;; (:score-at-least 5) → aggregate_all(sum(W), signal(Id, _, W), T), T >= 5
|
;; (:score-at-least 5) → aggregate_all(sum(W), signal(Id, _, W), T), T >= 5
|
||||||
|
;; (:reporters-at-least 2) → report(Id, _, Sr), setof(Br, report(_, Br, Sr), Bsr),
|
||||||
|
;; length(Bsr, Nr), Nr >= 2 (distinct reporters;
|
||||||
|
;; needs the quorum engine which asserts every report)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
mod/cond->goal
|
mod/cond->goal
|
||||||
@@ -78,6 +81,13 @@
|
|||||||
idterm
|
idterm
|
||||||
", _, W), T), T >= "
|
", _, W), T), T >= "
|
||||||
(nth c 1)))
|
(nth c 1)))
|
||||||
|
((= tag :reporters-at-least)
|
||||||
|
(str
|
||||||
|
"report("
|
||||||
|
idterm
|
||||||
|
", _, Sr), setof(Br, report(_, Br, Sr), Bsr), "
|
||||||
|
"length(Bsr, Nr), Nr >= "
|
||||||
|
(nth c 1)))
|
||||||
(true "true")))))
|
(true "true")))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
|
|||||||
40
lib/mod/quorum.sx
Normal file
40
lib/mod/quorum.sx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
;; lib/mod/quorum.sx — quorum decisions over distinct reporters (anti-brigade).
|
||||||
|
;;
|
||||||
|
;; The base engine asserts only the decided report's report/3 fact, so it can't
|
||||||
|
;; reason about WHO reported a subject. The quorum engine additionally asserts
|
||||||
|
;; every report's report/3 fact (via link's rel-facts), letting a rule require N
|
||||||
|
;; *distinct* reporters with `setof`/`length` — so one user filing many reports
|
||||||
|
;; does not manufacture consensus. Same decision shape as the base engine, plus
|
||||||
|
;; :strategy "quorum".
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod/build-quorum-program
|
||||||
|
(fn
|
||||||
|
(r count reports rules)
|
||||||
|
(str
|
||||||
|
(mod/report-rel-facts reports)
|
||||||
|
"\n"
|
||||||
|
(mod/report-facts r count)
|
||||||
|
"\n"
|
||||||
|
(mod/rules->program rules))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod/decide-quorum
|
||||||
|
(fn
|
||||||
|
(r reports rules)
|
||||||
|
(let
|
||||||
|
((count (mod/report-count (mod/report-about r) reports))
|
||||||
|
(kinds (mod/classify-keywords r))
|
||||||
|
(id (mod/report-id r)))
|
||||||
|
(let
|
||||||
|
((program (mod/build-quorum-program r count reports rules)))
|
||||||
|
(let
|
||||||
|
((db (pl-load program)))
|
||||||
|
(let
|
||||||
|
((sol (pl-query-one db (str "policy_action(" id ", Action, Rule)"))))
|
||||||
|
(if
|
||||||
|
(nil? sol)
|
||||||
|
{:action "keep" :proof {:goals (list) :evidence kinds :conditions (list) :rule "none" :count count} :report-id id :rule "none" :strategy "quorum"}
|
||||||
|
(let
|
||||||
|
((rule (mod/find-rule rules (dict-get sol "Rule"))))
|
||||||
|
{:action (mod/rule-action rule) :proof {:goals (mod/proof-goals db id (mod/rule-when rule)) :evidence kinds :conditions (mod/rule-when rule) :rule (mod/rule-name rule) :count count} :report-id id :rule (mod/rule-name rule) :strategy "quorum"}))))))))
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"lang": "mod",
|
"lang": "mod",
|
||||||
"total_passed": 223,
|
"total_passed": 232,
|
||||||
"total_failed": 0,
|
"total_failed": 0,
|
||||||
"total": 223,
|
"total": 232,
|
||||||
"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},
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
{"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}
|
{"name":"offenders","passed":19,"failed":0,"total":19},
|
||||||
|
{"name":"quorum","passed":9,"failed":0,"total":9}
|
||||||
],
|
],
|
||||||
"generated": "2026-06-06T18:29:10+00:00"
|
"generated": "2026-06-06T18:44:47+00:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# mod scoreboard
|
# mod scoreboard
|
||||||
|
|
||||||
**223 / 223 passing** (0 failure(s)).
|
**232 / 232 passing** (0 failure(s)).
|
||||||
|
|
||||||
| Suite | Passed | Total | Status |
|
| Suite | Passed | Total | Status |
|
||||||
|-------|--------|-------|--------|
|
|-------|--------|-------|--------|
|
||||||
@@ -13,3 +13,4 @@
|
|||||||
| lint | 14 | 14 | ok |
|
| lint | 14 | 14 | ok |
|
||||||
| severity | 14 | 14 | ok |
|
| severity | 14 | 14 | ok |
|
||||||
| offenders | 19 | 19 | ok |
|
| offenders | 19 | 19 | ok |
|
||||||
|
| quorum | 9 | 9 | ok |
|
||||||
|
|||||||
119
lib/mod/tests/quorum.sx
Normal file
119
lib/mod/tests/quorum.sx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
;; lib/mod/tests/quorum.sx — Ext 8: quorum over distinct reporters.
|
||||||
|
|
||||||
|
(define mod-q-count 0)
|
||||||
|
(define mod-q-pass 0)
|
||||||
|
(define mod-q-fail 0)
|
||||||
|
(define mod-q-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-q-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(begin
|
||||||
|
(set! mod-q-count (+ mod-q-count 1))
|
||||||
|
(if
|
||||||
|
(= got expected)
|
||||||
|
(set! mod-q-pass (+ mod-q-pass 1))
|
||||||
|
(begin
|
||||||
|
(set! mod-q-fail (+ mod-q-fail 1))
|
||||||
|
(append!
|
||||||
|
mod-q-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got)))))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-q-rules
|
||||||
|
(list
|
||||||
|
(mod/mk-rule
|
||||||
|
"quorum-hide"
|
||||||
|
:hide (list (list :reporters-at-least 2)))
|
||||||
|
(mod/mk-rule "default-keep" :keep (list))))
|
||||||
|
|
||||||
|
;; ── two distinct reporters meet quorum ──
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-q-two
|
||||||
|
(list
|
||||||
|
(mod/mk-report "r1" "alice" "bob" "off-topic")
|
||||||
|
(mod/mk-report "r2" "carol" "bob" "off-topic")))
|
||||||
|
|
||||||
|
(mod-q-test!
|
||||||
|
"two distinct reporters → hide"
|
||||||
|
(get (mod/decide-quorum (first mod-q-two) mod-q-two mod-q-rules) :action)
|
||||||
|
"hide")
|
||||||
|
(mod-q-test!
|
||||||
|
"quorum decision names the rule"
|
||||||
|
(get (mod/decide-quorum (first mod-q-two) mod-q-two mod-q-rules) :rule)
|
||||||
|
"quorum-hide")
|
||||||
|
(mod-q-test!
|
||||||
|
"quorum decision tagged strategy"
|
||||||
|
(get (mod/decide-quorum (first mod-q-two) mod-q-two mod-q-rules) :strategy)
|
||||||
|
"quorum")
|
||||||
|
|
||||||
|
;; ── single reporter does not meet quorum ──
|
||||||
|
|
||||||
|
(define mod-q-one (list (mod/mk-report "r1" "alice" "bob" "off-topic")))
|
||||||
|
(mod-q-test!
|
||||||
|
"one reporter → keep (below quorum)"
|
||||||
|
(get (mod/decide-quorum (first mod-q-one) mod-q-one mod-q-rules) :action)
|
||||||
|
"keep")
|
||||||
|
|
||||||
|
;; ── anti-brigade: one user filing many reports does NOT meet quorum ──
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-q-brigade
|
||||||
|
(list
|
||||||
|
(mod/mk-report "r1" "alice" "bob" "off-topic")
|
||||||
|
(mod/mk-report "r2" "alice" "bob" "off-topic")
|
||||||
|
(mod/mk-report "r3" "alice" "bob" "off-topic")))
|
||||||
|
|
||||||
|
(mod-q-test!
|
||||||
|
"three reports, one reporter → keep (quorum counts distinct)"
|
||||||
|
(get
|
||||||
|
(mod/decide-quorum (first mod-q-brigade) mod-q-brigade mod-q-rules)
|
||||||
|
:action)
|
||||||
|
"keep")
|
||||||
|
|
||||||
|
;; contrast: the count rule WOULD fire on the same brigade (3 reports ≥ 3) —
|
||||||
|
;; quorum is strictly stronger against single-actor brigading
|
||||||
|
(mod-q-test!
|
||||||
|
"count rule fires on the brigade (distinct from quorum)"
|
||||||
|
(get
|
||||||
|
(mod/decide-report (first mod-q-brigade) mod-q-brigade mod/default-rules)
|
||||||
|
:action)
|
||||||
|
"escalate")
|
||||||
|
|
||||||
|
;; ── three distinct reporters ──
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-q-three
|
||||||
|
(list
|
||||||
|
(mod/mk-report "r1" "alice" "bob" "off-topic")
|
||||||
|
(mod/mk-report "r2" "carol" "bob" "off-topic")
|
||||||
|
(mod/mk-report "r3" "dave" "bob" "off-topic")))
|
||||||
|
|
||||||
|
(mod-q-test!
|
||||||
|
"three distinct reporters → hide"
|
||||||
|
(get
|
||||||
|
(mod/decide-quorum (first mod-q-three) mod-q-three mod-q-rules)
|
||||||
|
:action)
|
||||||
|
"hide")
|
||||||
|
(mod-q-test!
|
||||||
|
"quorum proof goal solved"
|
||||||
|
(get
|
||||||
|
(first
|
||||||
|
(get
|
||||||
|
(get
|
||||||
|
(mod/decide-quorum (first mod-q-three) mod-q-three mod-q-rules)
|
||||||
|
:proof)
|
||||||
|
:goals))
|
||||||
|
:solved)
|
||||||
|
true)
|
||||||
|
|
||||||
|
;; ── cond->goal compiles :reporters-at-least ──
|
||||||
|
|
||||||
|
(mod-q-test!
|
||||||
|
"cond->goal :reporters-at-least"
|
||||||
|
(mod/cond->goal (list :reporters-at-least 2) "Id")
|
||||||
|
"report(Id, _, Sr), setof(Br, report(_, Br, Sr), Bsr), length(Bsr, Nr), Nr >= 2")
|
||||||
|
|
||||||
|
(define mod-quorum-tests-run! (fn () {:failures mod-q-failures :total mod-q-count :passed mod-q-pass :failed mod-q-fail}))
|
||||||
@@ -16,7 +16,7 @@ federation extension.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/mod/conformance.sh` → **223/223** (roadmap + 7 extensions complete)
|
`bash lib/mod/conformance.sh` → **232/232** (roadmap + 8 extensions complete)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -147,6 +147,14 @@ 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 8 — quorum over distinct reporters** (`lib/mod/quorum.sx`, +9). Anti-
|
||||||
|
brigade: `(:reporters-at-least N)` compiles to `setof(Br, report(_, Br, Sr), Bsr),
|
||||||
|
length(Bsr, Nr), Nr >= N` — distinct reporters, not raw report count.
|
||||||
|
`mod/decide-quorum` asserts every report's `report/3` fact (the base engine only
|
||||||
|
asserts the decided one) so Prolog can aggregate reporters. Verified one user
|
||||||
|
filing 3 reports stays `:keep` under quorum while the count rule would escalate.
|
||||||
|
(Substrate note: `^` existential doesn't parse; `setof(B, p(_, B, S), …)` with `_`
|
||||||
|
yields the distinct set in a single solution here.)
|
||||||
- [x] **Ext 7 — repeat-offender escalation** (`lib/mod/offenders.sx`, +19). The
|
- [x] **Ext 7 — repeat-offender escalation** (`lib/mod/offenders.sx`, +19). The
|
||||||
audit log as evidence: `mod/subject-sanctions` counts prior hide/remove/ban
|
audit log as evidence: `mod/subject-sanctions` counts prior hide/remove/ban
|
||||||
decisions about a subject; `mod/decide-escalating id k` decides normally then
|
decisions about a subject; `mod/decide-escalating id k` decides normally then
|
||||||
@@ -172,6 +180,13 @@ lib/mod/fed.sx
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- **Ext 8 — quorum over distinct reporters, 232/232** (+9). Distinct-reporter
|
||||||
|
consensus via Prolog `setof`/`length`, requiring a second engine variant that
|
||||||
|
asserts all reports (the base engine deliberately scopes facts to the decided
|
||||||
|
report). Demonstrates the substrate handles set-aggregation, and that the
|
||||||
|
brigade case (one actor, many reports) is defeated by counting reporters not
|
||||||
|
reports. `^` existential doesn't parse here — `setof(B, p(_,B,S), …)` with `_`
|
||||||
|
gives the distinct set in one solution.
|
||||||
- **Ext 7 — repeat-offender escalation, 223/223** (+19). Decisions can now depend
|
- **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
|
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
|
with k prior sanctions has its next sanction upgraded to `:ban`. Closes the loop
|
||||||
|
|||||||
Reference in New Issue
Block a user