Reports carry an :evidence list, asserted as evidence/3 facts; reviewer-remove rule (highest precedence) lets human review override classification. Proof tree built constructively by re-querying each rule body goal against the same DB with the report id bound, so derivations carry real unification bindings. Append-only audit log records decision + proof + evidence snapshot per decide, monotonic seq, never mutates prior entries. +29 audit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
mod-on-sx: Moderation on Prolog
rose-ash needs moderation infrastructure: reports flagged by users, automated classifications (spam, abuse), tiered escalation (auto → human → appeal), audit trails. Each decision is the conclusion of a backtracking search over evidence and policy rules — exactly what Prolog does.
Where acl-sx says "may this happen?", mod-sx says "should this stay?" The former is a positive decision (proof of grant); the latter often a negative one (proof of violation), and policy chains naturally backtrack: if the first rule doesn't apply, try the next.
End-state: a Prolog-on-SX layer for moderation policy declaration and evaluation, with persistent report lifecycle, audit log, escalation state machine, and federation extension.
Status (rolling)
bash lib/mod/conformance.sh → 60/60 (Phases 1–2 complete)
Ground rules
- Scope: only touch
lib/mod/**andplans/mod-on-sx.md. Do not editspec/,hosts/,shared/,lib/prolog/**, or otherlib/<lang>/. You may import fromlib/prolog/(public API inlib/prolog/prolog.sx); do not modify Prolog. - Shared-file issues go under "Blockers" with a minimal repro; do not fix here.
- SX files: use
sx-treeMCP tools only. - Architecture: policies are Prolog rules over
report(...)andevidence(...)facts. Decisions are query results. Proof trees become audit records. The state machine for report lifecycle is separate (an SX module on top). - Shared with acl-sx: rule-engine plumbing may be liftable into
lib/guest/. Watch for it; flag in Progress log but do not extract until both subsystems are past Phase 2. - Commits: one feature per commit. Keep Progress log updated and tick boxes.
Architecture sketch
Report Decision
{:by :about :reason :at} {:action :proof :next-state}
│ ▲
▼ │
lib/mod/schema.sx lib/mod/engine.sx
— report/4, evidence/2, — query Prolog with report fact
classification/3 predicates — extract proof tree
│ ▲
▼ │
lib/mod/policy.sx lib/mod/lifecycle.sx
— rule syntax → Prolog — state machine
— action heads: — open → triaged → decided
{:keep :hide :remove — appeal handling
:escalate :ban} │
│ ▼
▼ lib/mod/audit.sx
lib/mod/api.sx — append-only decision log
— (mod/report ...) — proof tree persistence
— (mod/decide report) — query API
— (mod/appeal id)
│
▼
lib/mod/fed.sx
— cross-instance reports via fed-sx
— decision sharing / trust model
Phase 1 — Report representation + simple policy
lib/mod/schema.sx—report(id, by, about),classification(id, kind),report_count(subject, n)Prolog facts; keyword classifier derives evidencelib/mod/policy.sx—mod/mk-rule+ orderedmod/default-rules; conditions (:classification,:count-at-least) compile to Prolog goals;policy_action/3clauses, last clausetrueso every report yields at least:keeplib/mod/engine.sx—(mod/decide-report r reports rules)queriespolicy_action(Id, Action, Rule)withpl-query-one(clause order = precedence); returns a decision dict{:action :rule :report-id :proof}carrying the whylib/mod/api.sx— registry +(mod/report by about reason),(mod/decide id)lib/mod/tests/decide.sx— 31 cases: spam/abuse keyword, repeated→escalate, no-rule→keep, precedence (spam beats repeated), proof shape, registry idslib/mod/scoreboard.{json,md}lib/mod/conformance.sh
Phase 2 — Evidence + audit trail
- evidence accumulation —
report :evidencelist;mod/attach-evidence+ apimod/add-evidence; asserted asevidence(Id, 'kind', 'val')facts; new:evidencecondition +reviewer-removerule consume it - proof tree from Prolog derivation —
mod/proof-goalsre-queries each body goal (id bound) against the same DB, recording goal text, solved?, and the bindings that satisfied it (e.g. count goal yields N=3, S=subject) lib/mod/audit.sx— append-only log: monotonic:seq, decision + proof + evidence snapshot; never mutates prior entries(mod/audit id)retrieval (+mod/audit-latest,mod/audit-all, count)lib/mod/tests/audit.sx— 29 cases: proof goal text/bindings, evidence-driven decisions, append-only ordering, per-report retrieval, snapshot-at-decision-time
Phase 3 — Escalation + lifecycle state machine
- state machine:
:open → :triaged → :decided → :appealed → :final - auto-tier: first-pass rules decide quick cases
- human-tier: rules that emit
:escalatemove to next state - appeal: re-runs with appeal evidence, may override prior decision
(mod/appeal id new-evidence)APIlib/mod/tests/escalation.sx— full lifecycle traversal cases
Phase 4 — Federation
- cross-instance reports — peer raises report about local content (or vice versa)
- decision sharing — actions taken locally propagate to peers via fed-sx
- trust model — peer's decision is advisory unless
(trust peer :mod)is granted - revocation — undo applied moderation if proof was invalidated
lib/mod/tests/fed.sx— federated decision chains (mock fed-sx in tests)
Progress log
- Phase 2 complete — 60/60 (+29 audit). Evidence accumulation, constructive
proof trees, append-only audit log. A decision's
:proof :goalsis a real derivation: each body goal is re-queried against the same Prolog DB with the report id bound, so the count rule's proof carriesN=3, S=<subject>straight from unification — not a reconstruction. Evidence is asserted asevidence(Id, 'kind', 'val'); the newreviewer-removerule (placed first = highest precedence) lets human review override automated classification.mod/decidenow commits each decision to the audit log with the evidence snapshot in force at decision time. Unknown predicates in this Prolog fail gracefully (verified) — so an evidence-less report safely falls through the reviewer rule without an existence error.- Liftable (acl-sx watch): the proof-tree builder (
mod/proof-goals— re-query-each-goal) and the append-only log shape are both generic. Both subsystems are now past Phase 2; next time either touches plumbing, evaluate liftingproof-goals+ the audit-log primitives intolib/guest/.
- Liftable (acl-sx watch): the proof-tree builder (
- Phase 1 complete — 31/31. Report schema, keyword classifier, policy DSL,
engine, registry api, conformance harness. Decisions are proofs: each carries
:rule(matching clause),:proof {:rule :conditions :evidence :count}. Precedence is Prolog clause order resolved bypl-query-one; a trailingtrue-bodied default rule makes "no rule matched" a real:keep, not a query failure. Evidence (spam/abuse classification) derived in SX and asserted asclassification/2facts; repeated-report escalation uses a genuine Prolog join + arithmetic (report(Id,_,S), report_count(S,N), N >= 3).- Gotcha (env): loading the prolog libs strips
includes?(and other high-level string prims) from the eval env — only the set the prolog tokenizer itself uses survives (slice,len,nth,=,join,downcase,map,reduce,append!). Implementedmod/str-contains?overslice/lenrather than relying onincludes?. Watch for this in later phases — stick to the blessed primitive set. - Liftable (acl-sx watch):
mod/join-with,mod/str-contains?,mod/any?, and the rule→clause compilation shape are generic rule-engine plumbing. Do not extract tolib/guest/until both mod-sx and acl-sx are past Phase 2.
- Gotcha (env): loading the prolog libs strips
Blockers
(none)