Files
rose-ash/plans/mod-on-sx.md
giles f4f34c1d33
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s
mod: Phase 3 — lifecycle state machine + escalation + appeal, 106/106
Pure SX state machine (lib/mod/lifecycle.sx) over the engine:
open→triaged→decided→appealed→final, transition table guards illegal moves.
Auto-tier resolves terminal actions; escalate parks at human-tier (resolve
blocked until review supplies evidence). Appeal re-runs the engine — new
exonerated-keep rule at top precedence lets exoneration override a prior hide.
Api façade (mod/triage/resolve/review/appeal/finalize) over a case registry,
logging committed decisions to the audit trail. +46 escalation tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:50:05 +00:00

10 KiB
Raw Blame History

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.sh106/106 (Phases 13 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/<lang>/. 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

  • lib/mod/schema.sxreport(id, by, about), classification(id, kind), report_count(subject, n) Prolog facts; keyword classifier derives evidence
  • lib/mod/policy.sxmod/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
  • 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
  • lib/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 ids
  • lib/mod/scoreboard.{json,md}
  • lib/mod/conformance.sh

Phase 2 — Evidence + audit trail

  • evidence accumulation — report :evidence list; mod/attach-evidence + api mod/add-evidence; asserted as evidence(Id, 'kind', 'val') facts; new :evidence condition + reviewer-remove rule consume it
  • proof tree from Prolog derivation — mod/proof-goals re-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: lib/mod/lifecycle.sx:open → :triaged → :decided → :appealed → :final as a pure SX module over the engine; transition table guards illegal moves (sets :error, leaves state); immutable cases with :history
  • auto-tier: mod/case-triage runs the engine; terminal action (hide/remove/ keep) → tier auto, mod/case-resolve advances to :decided
  • human-tier: :escalate action → tier human; mod/case-resolve is blocked (sets :error); mod/case-review attaches evidence, re-decides, advances
  • appeal: mod/case-appeal attaches appeal evidence + re-runs the engine; new exonerated-keep rule (top precedence) lets exoneration override a prior :hide
  • (mod/appeal id new-evidence) API — lifecycle façade over a case registry in api.sx (mod/triage / resolve / review / appeal / finalize), logging each committed decision to the audit trail
  • lib/mod/tests/escalation.sx — 46 cases: transition guards, auto/human tiers, blocked resolve, full appeal-override traversal, history, api façade

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 3 complete — 106/106 (+46 escalation). Lifecycle state machine, auto/human tiers, appeal-override, and an api façade. The state machine is a pure SX module (lib/mod/lifecycle.sx) over the engine — policy stays in Prolog, lifecycle stays out of it, per the design constraint. Cases are immutable values threaded through transitions; illegal moves set :error rather than throwing (the env's error handling is untested, so this keeps tests deterministic). Tier logic: triage runs the engine, an :escalate action parks the case at the human tier where mod/case-resolve is blocked until mod/case-review supplies evidence. Appeal-override works because the new exonerated-keep rule sits at top precedence — appeal evidence re-runs the same engine and a higher-precedence clause wins. The api façade (mod/triagemod/finalize) keeps a per-report case registry and logs each committed decision to the Phase-2 audit trail, so lifecycle + audit compose.
    • Gotcha: sx_insert_near inserts only the FIRST top-level form of a multi-form source — silently drops the rest (byte count barely changes). For multi-form additions, rewrite the file with sx_write_file.
  • Phase 2 complete — 60/60 (+29 audit). Evidence accumulation, constructive proof trees, append-only audit log. A decision's :proof :goals is 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 carries N=3, S=<subject> straight from unification — not a reconstruction. Evidence is asserted as evidence(Id, 'kind', 'val'); the new reviewer-remove rule (placed first = highest precedence) lets human review override automated classification. mod/decide now 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 lifting proof-goals + the audit-log primitives into lib/guest/.
  • 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)