# 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` → **31/31** (Phase 1 complete) ## Ground rules - **Scope:** only touch `lib/mod/**` and `plans/mod-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/prolog/**`, or other `lib//`. You may **import** from `lib/prolog/` (public API in `lib/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-tree` MCP tools only. - **Architecture:** policies are Prolog rules over `report(...)` and `evidence(...)` 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 - [x] `lib/mod/schema.sx` — `report(id, by, about)`, `classification(id, kind)`, `report_count(subject, n)` Prolog facts; keyword classifier derives evidence - [x] `lib/mod/policy.sx` — `mod/mk-rule` + ordered `mod/default-rules`; conditions (`:classification`, `:count-at-least`) compile to Prolog goals; `policy_action/3` clauses, last clause `true` so every report yields at least `:keep` - [x] `lib/mod/engine.sx` — `(mod/decide-report r reports rules)` queries `policy_action(Id, Action, Rule)` with `pl-query-one` (clause order = precedence); returns a decision dict `{:action :rule :report-id :proof}` carrying the why - [x] `lib/mod/api.sx` — registry + `(mod/report by about reason)`, `(mod/decide id)` - [x] `lib/mod/tests/decide.sx` — 31 cases: spam/abuse keyword, repeated→escalate, no-rule→keep, precedence (spam beats repeated), proof shape, registry ids - [x] `lib/mod/scoreboard.{json,md}` - [x] `lib/mod/conformance.sh` ## Phase 2 — Evidence + audit trail - [ ] evidence accumulation — additional facts asserted before query - [ ] proof tree from Prolog derivation tree - [ ] `lib/mod/audit.sx` — append-only log (decision + proof + evidence snapshot) - [ ] `(mod/audit id)` retrieval - [ ] `lib/mod/tests/audit.sx` — proof correctness, trail completeness ## 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 `:escalate` move to next state - [ ] appeal: re-runs with appeal evidence, may override prior decision - [ ] `(mod/appeal id new-evidence)` API - [ ] `lib/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 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 by `pl-query-one`; a trailing `true`-bodied default rule makes "no rule matched" a real `:keep`, not a query failure. Evidence (spam/abuse classification) derived in SX and asserted as `classification/2` facts; 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!`). Implemented `mod/str-contains?` over `slice`/`len` rather than relying on `includes?`. 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 to `lib/guest/` until both mod-sx and acl-sx are past Phase 2. ## Blockers (none)