Files
rose-ash/plans/mod-on-sx.md
giles 6e825e1283
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m7s
mod: Phase 2 — evidence accumulation + proof trees + audit log, 60/60
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>
2026-06-06 17:37:02 +00:00

152 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 12 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
- [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
- [x] 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
- [x] 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)
- [x] `lib/mod/audit.sx` — append-only log: monotonic `:seq`, decision + proof +
evidence snapshot; never mutates prior entries
- [x] `(mod/audit id)` retrieval (+ `mod/audit-latest`, `mod/audit-all`, count)
- [x] `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 `: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 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)