mod: Ext 15 — disjunctive (:any) conditions, 333/333
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 29s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 29s
(:any (list c1 c2 ...)) compiles to Prolog disjunction (g1 ; g2 ; ...), completing the condition boolean algebra (AND via :when list, :not, :any). cond->goal recurses so combinators nest arbitrarily; the proof tree shows the compiled disjunction verbatim. Maps onto Prolog's control constructs rather than reimplementing boolean logic in SX. +10 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,4 +48,5 @@ SUITES=(
|
|||||||
"temporal:lib/mod/tests/temporal.sx:(mod-temporal-tests-run!)"
|
"temporal:lib/mod/tests/temporal.sx:(mod-temporal-tests-run!)"
|
||||||
"sla:lib/mod/tests/sla.sx:(mod-sla-tests-run!)"
|
"sla:lib/mod/tests/sla.sx:(mod-sla-tests-run!)"
|
||||||
"wire:lib/mod/tests/wire.sx:(mod-wire-tests-run!)"
|
"wire:lib/mod/tests/wire.sx:(mod-wire-tests-run!)"
|
||||||
|
"disjunction:lib/mod/tests/disjunction.sx:(mod-disjunction-tests-run!)"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
;; lib/mod/policy.sx — moderation rules → Prolog clauses.
|
;; lib/mod/policy.sx — moderation rules → Prolog clauses.
|
||||||
;;
|
;;
|
||||||
;; A rule is {:name :action :when}. :when is a list of condition forms; each
|
;; A rule is {:name :action :when}. :when is a list of condition forms; each
|
||||||
;; compiles to a Prolog goal. Rule order is precedence: the engine queries with
|
;; compiles to a Prolog goal. The conditions in a :when list are ANDed (joined by
|
||||||
;; pl-query-one, so the first clause that proves wins. The final default rule has
|
;; ", "); :not negates and :any (a list of sub-conditions) disjoins — so the
|
||||||
;; an empty body (true) so every report yields at least :keep — "no rule matched"
|
;; condition language is a small boolean algebra over the leaf predicates.
|
||||||
;; is a real result, not a query failure.
|
;; Rule order is precedence: the engine queries with pl-query-one, so the first
|
||||||
|
;; clause that proves wins. The final default rule has an empty body (true) so
|
||||||
|
;; every report yields at least :keep — "no rule matched" is a real result, not a
|
||||||
|
;; query failure.
|
||||||
;;
|
;;
|
||||||
;; cond->goal takes an id-term so the same condition can be compiled with the
|
;; cond->goal takes an id-term so the same condition can be compiled with the
|
||||||
;; head variable "Id" (for clause bodies) or a concrete report id (for proof-tree
|
;; head variable "Id" (for clause bodies) or a concrete report id (for proof-tree
|
||||||
@@ -43,14 +46,14 @@
|
|||||||
;; (: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)
|
||||||
|
;; (:any (list c1 c2 ...)) → (g1 ; g2 ; ...) (disjunction)
|
||||||
;; (: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),
|
;; (:reporters-at-least 2) → report(Id, _, Sr), setof(Br, report(_, Br, Sr), Bsr),
|
||||||
;; length(Bsr, Nr), Nr >= 2 (distinct reporters;
|
;; length(Bsr, Nr), Nr >= 2 (quorum engine)
|
||||||
;; needs the quorum engine which asserts every report)
|
|
||||||
;; (:burst-at-least 3) → report(Id, _, Sb), burst_count(Sb, Nb), Nb >= 3
|
;; (:burst-at-least 3) → report(Id, _, Sb), burst_count(Sb, Nb), Nb >= 3
|
||||||
;; (reports in a time window; needs the temporal engine)
|
;; (temporal engine)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
mod/cond->goal
|
mod/cond->goal
|
||||||
@@ -71,6 +74,15 @@
|
|||||||
((= tag :attr) (str "attr(" idterm ", " (nth c 1) ")"))
|
((= tag :attr) (str "attr(" idterm ", " (nth c 1) ")"))
|
||||||
((= tag :not)
|
((= tag :not)
|
||||||
(str "not(" (mod/cond->goal (nth c 1) idterm) ")"))
|
(str "not(" (mod/cond->goal (nth c 1) idterm) ")"))
|
||||||
|
((= tag :any)
|
||||||
|
(str
|
||||||
|
"("
|
||||||
|
(mod/join-with
|
||||||
|
" ; "
|
||||||
|
(map
|
||||||
|
(fn (sub) (mod/cond->goal sub idterm))
|
||||||
|
(nth c 1)))
|
||||||
|
")"))
|
||||||
((= tag :count-at-least)
|
((= tag :count-at-least)
|
||||||
(str
|
(str
|
||||||
"report("
|
"report("
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"lang": "mod",
|
"lang": "mod",
|
||||||
"total_passed": 323,
|
"total_passed": 333,
|
||||||
"total_failed": 0,
|
"total_failed": 0,
|
||||||
"total": 323,
|
"total": 333,
|
||||||
"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},
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
{"name":"batch","passed":17,"failed":0,"total":17},
|
{"name":"batch","passed":17,"failed":0,"total":17},
|
||||||
{"name":"temporal","passed":15,"failed":0,"total":15},
|
{"name":"temporal","passed":15,"failed":0,"total":15},
|
||||||
{"name":"sla","passed":15,"failed":0,"total":15},
|
{"name":"sla","passed":15,"failed":0,"total":15},
|
||||||
{"name":"wire","passed":16,"failed":0,"total":16}
|
{"name":"wire","passed":16,"failed":0,"total":16},
|
||||||
|
{"name":"disjunction","passed":10,"failed":0,"total":10}
|
||||||
],
|
],
|
||||||
"generated": "2026-06-06T19:16:49+00:00"
|
"generated": "2026-06-06T19:22:42+00:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# mod scoreboard
|
# mod scoreboard
|
||||||
|
|
||||||
**323 / 323 passing** (0 failure(s)).
|
**333 / 333 passing** (0 failure(s)).
|
||||||
|
|
||||||
| Suite | Passed | Total | Status |
|
| Suite | Passed | Total | Status |
|
||||||
|-------|--------|-------|--------|
|
|-------|--------|-------|--------|
|
||||||
@@ -20,3 +20,4 @@
|
|||||||
| temporal | 15 | 15 | ok |
|
| temporal | 15 | 15 | ok |
|
||||||
| sla | 15 | 15 | ok |
|
| sla | 15 | 15 | ok |
|
||||||
| wire | 16 | 16 | ok |
|
| wire | 16 | 16 | ok |
|
||||||
|
| disjunction | 10 | 10 | ok |
|
||||||
|
|||||||
145
lib/mod/tests/disjunction.sx
Normal file
145
lib/mod/tests/disjunction.sx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
;; lib/mod/tests/disjunction.sx — Ext 15: disjunctive (:any) conditions.
|
||||||
|
|
||||||
|
(define mod-or-count 0)
|
||||||
|
(define mod-or-pass 0)
|
||||||
|
(define mod-or-fail 0)
|
||||||
|
(define mod-or-failures (list))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-or-test!
|
||||||
|
(fn
|
||||||
|
(name got expected)
|
||||||
|
(begin
|
||||||
|
(set! mod-or-count (+ mod-or-count 1))
|
||||||
|
(if
|
||||||
|
(= got expected)
|
||||||
|
(set! mod-or-pass (+ mod-or-pass 1))
|
||||||
|
(begin
|
||||||
|
(set! mod-or-fail (+ mod-or-fail 1))
|
||||||
|
(append!
|
||||||
|
mod-or-failures
|
||||||
|
(str name "\n expected: " expected "\n got: " got)))))))
|
||||||
|
|
||||||
|
;; one rule, OR of two classifications → one action covers both
|
||||||
|
(define
|
||||||
|
mod-or-rules
|
||||||
|
(list
|
||||||
|
(mod/mk-rule
|
||||||
|
"spam-or-abuse-hide"
|
||||||
|
:hide (list
|
||||||
|
(list
|
||||||
|
:any (list (list :classification "spam") (list :classification "abuse")))))
|
||||||
|
(mod/mk-rule "default-keep" :keep (list))))
|
||||||
|
|
||||||
|
(define mod-or-spam (mod/mk-report "r1" "a" "b" "this is spam"))
|
||||||
|
(define mod-or-abuse (mod/mk-report "r2" "a" "b" "harassment here"))
|
||||||
|
(define mod-or-clean (mod/mk-report "r3" "a" "b" "a fine post"))
|
||||||
|
|
||||||
|
(mod-or-test!
|
||||||
|
"OR: spam branch → hide"
|
||||||
|
(get
|
||||||
|
(mod/decide-report mod-or-spam (list mod-or-spam) mod-or-rules)
|
||||||
|
:action)
|
||||||
|
"hide")
|
||||||
|
(mod-or-test!
|
||||||
|
"OR: abuse branch → hide"
|
||||||
|
(get
|
||||||
|
(mod/decide-report mod-or-abuse (list mod-or-abuse) mod-or-rules)
|
||||||
|
:action)
|
||||||
|
"hide")
|
||||||
|
(mod-or-test!
|
||||||
|
"OR: neither branch → keep"
|
||||||
|
(get
|
||||||
|
(mod/decide-report mod-or-clean (list mod-or-clean) mod-or-rules)
|
||||||
|
:action)
|
||||||
|
"keep")
|
||||||
|
|
||||||
|
;; ── goal text + proof ──
|
||||||
|
|
||||||
|
(mod-or-test!
|
||||||
|
"cond->goal :any joins with ;"
|
||||||
|
(mod/cond->goal
|
||||||
|
(list
|
||||||
|
:any (list (list :classification "spam") (list :classification "abuse")))
|
||||||
|
"Id")
|
||||||
|
"(classification(Id, spam) ; classification(Id, abuse))")
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-or-dec
|
||||||
|
(mod/decide-report mod-or-spam (list mod-or-spam) mod-or-rules))
|
||||||
|
(mod-or-test!
|
||||||
|
"OR proof goal solved"
|
||||||
|
(get (first (get (get mod-or-dec :proof) :goals)) :solved)
|
||||||
|
true)
|
||||||
|
(mod-or-test!
|
||||||
|
"OR proof goal text"
|
||||||
|
(get (first (get (get mod-or-dec :proof) :goals)) :goal)
|
||||||
|
"(classification(r1, spam) ; classification(r1, abuse))")
|
||||||
|
|
||||||
|
;; ── :any composes with :not (NOR-ish) and :attr ──
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-or-mixed-rules
|
||||||
|
(list
|
||||||
|
(mod/mk-rule
|
||||||
|
"spam-or-flagged-hide"
|
||||||
|
:hide (list
|
||||||
|
(list
|
||||||
|
:any (list (list :classification "spam") (list :attr "flagged")))))
|
||||||
|
(mod/mk-rule "default-keep" :keep (list))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-or-flagged
|
||||||
|
(mod/attach-attr (mod/mk-report "r4" "a" "b" "a fine post") "flagged"))
|
||||||
|
(mod-or-test!
|
||||||
|
"OR over classification|attr: flagged clean post → hide"
|
||||||
|
(get
|
||||||
|
(mod/decide-report
|
||||||
|
mod-or-flagged
|
||||||
|
(list mod-or-flagged)
|
||||||
|
mod-or-mixed-rules)
|
||||||
|
:action)
|
||||||
|
"hide")
|
||||||
|
|
||||||
|
(mod-or-test!
|
||||||
|
"cond->goal :any with :not branch"
|
||||||
|
(mod/cond->goal
|
||||||
|
(list
|
||||||
|
:any (list
|
||||||
|
(list :classification "spam")
|
||||||
|
(list :not (list :attr "verified"))))
|
||||||
|
"Id")
|
||||||
|
"(classification(Id, spam) ; not(attr(Id, verified)))")
|
||||||
|
|
||||||
|
;; AND still works alongside OR in the same :when list
|
||||||
|
(define
|
||||||
|
mod-or-and-rules
|
||||||
|
(list
|
||||||
|
(mod/mk-rule
|
||||||
|
"spam-and-not-verified"
|
||||||
|
:hide (list
|
||||||
|
(list
|
||||||
|
:any (list (list :classification "spam") (list :classification "abuse")))
|
||||||
|
(list :not (list :attr "verified"))))
|
||||||
|
(mod/mk-rule "default-keep" :keep (list))))
|
||||||
|
|
||||||
|
(define
|
||||||
|
mod-or-spam-verified
|
||||||
|
(mod/attach-attr (mod/mk-report "r5" "a" "b" "this is spam") "verified"))
|
||||||
|
(mod-or-test!
|
||||||
|
"AND of OR + NOT: verified spam → keep"
|
||||||
|
(get
|
||||||
|
(mod/decide-report
|
||||||
|
mod-or-spam-verified
|
||||||
|
(list mod-or-spam-verified)
|
||||||
|
mod-or-and-rules)
|
||||||
|
:action)
|
||||||
|
"keep")
|
||||||
|
(mod-or-test!
|
||||||
|
"AND of OR + NOT: unverified abuse → hide"
|
||||||
|
(get
|
||||||
|
(mod/decide-report mod-or-abuse (list mod-or-abuse) mod-or-and-rules)
|
||||||
|
:action)
|
||||||
|
"hide")
|
||||||
|
|
||||||
|
(define mod-disjunction-tests-run! (fn () {:failures mod-or-failures :total mod-or-count :passed mod-or-pass :failed mod-or-fail}))
|
||||||
@@ -16,7 +16,7 @@ federation extension.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/mod/conformance.sh` → **323/323** (roadmap + 14 extensions complete)
|
`bash lib/mod/conformance.sh` → **333/333** (roadmap + 15 extensions complete)
|
||||||
|
|
||||||
## Ground rules
|
## Ground rules
|
||||||
|
|
||||||
@@ -147,6 +147,11 @@ 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 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`).
|
||||||
|
Composes recursively — `:any` over `:not`/`:attr`/classification, and ANDs with
|
||||||
|
other conditions in the same rule. One rule now covers "spam OR abuse".
|
||||||
- [x] **Ext 14 — decision wire format** (`lib/mod/wire.sx`, +16). The bytes that
|
- [x] **Ext 14 — decision wire format** (`lib/mod/wire.sx`, +16). The bytes that
|
||||||
cross `fed/fed-send!`: `mod/decision->wire` emits a versioned pipe-delimited line
|
cross `fed/fed-send!`: `mod/decision->wire` emits a versioned pipe-delimited line
|
||||||
(`MOD1|r1|hide|spam-hide`), `mod/wire->decision` parses it back (`mod/wire-valid?`
|
(`MOD1|r1|hide|spam-hide`), `mod/wire->decision` parses it back (`mod/wire-valid?`
|
||||||
@@ -213,6 +218,12 @@ lib/mod/fed.sx
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- **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
|
||||||
|
`:any` ANDed with a `:not`, etc. — and the proof tree shows the compiled
|
||||||
|
disjunction verbatim. Maps directly onto Prolog's own control constructs rather
|
||||||
|
than reimplementing boolean logic in SX.
|
||||||
- **Ext 14 — decision wire format, 323/323** (+16). Fills the federation transport
|
- **Ext 14 — decision wire format, 323/323** (+16). Fills the federation transport
|
||||||
seam: decisions now serialize to a portable line and parse back, and the
|
seam: decisions now serialize to a portable line and parse back, and the
|
||||||
integration test runs the whole federated path end-to-end (serialize on one
|
integration test runs the whole federated path end-to-end (serialize on one
|
||||||
|
|||||||
Reference in New Issue
Block a user