Foundation Moderation

  • Area: Foundation
  • Path: foundation/moderation
  • Kind: Content moderation, DMCA, fingerprint (RFC-001)

Role in the stack

koder-moderation powers the report-and-strike + fingerprint side of products/horizontal/play/engine (Koder Play). Reports come in via intake, get triaged into queues, accumulate strike weight on the offending channel, optionally fingerprint-match against a per-tenant Content-ID corpus, and wire into automod policy when classifiers fire above thresholds.

Deployed as koder-moderation-server (axum, default 0.0.0.0:8000); RUST_LOG controls tracing; /metrics exposed for Prometheus.

Substrate

  • Intake: in-memory v0.1; production swap to data/kdb kdb-record. SubmitArgs builder; classifier-signal injection per (targetkind, targetid). Reporter PII hashed at intake (#016): reporter_emailreporter_user_id are salt+SHA-256'd (hash_pii, with_saltMODERATION_REPORTER_SALT) before persist — raw identifiers never reach storage/audit; deterministic so dedup still works.
  • Strikes: StrikeLedger with 90-day linear decay; severity per StrikeKind. prune_decayed() (#025) is the bulk decay job — drops fully-decayed (>90d) strikes + empties channels, weight-preserving; periodic scheduling is serve-time (like ClassifierCache::evict_stale).
  • Fingerprint: FingerprintCorpus per tenant; Hamming similarity v0.1.
  • Extractor (MOD-010): FingerprintExtractor trait + StubExtractor (always-on) + feature-gated chromaprintPDQ wraps (queued MOD-012#013).
  • LSH index (MOD-010): LshIndex buckets by 16-bit chunks for sub-millisecond candidate retrieval at million-entry corpus.
  • Classifier bridge (MOD-011): Classifier trait + LocalRuleClassifier (keyword fallback, conf cap 0.85) + ClassifierCache (24h TTL + max-size LRU, #024: DEFAULT_MAX_ENTRIES = 50_000, with_ttl_and_max, per-entry AtomicU64 recency bumped under the read lock so reads stay concurrent; max_size 0 = unbounded).
  • Automod: AutoModPolicyTable decides Ignore / ReviewQueue / AutoHide on decide_many(labels).

Key features

  • [x] Intake submit with triage in single call (MOD-001/#002).
  • [x] Strike ledger with 90-day decay (MOD-003).
  • [x] Automod policy table (MOD-004).
  • [x] Fingerprint corpus + similarity scoring (MOD-005).
  • [x] HTTP API + Prometheus + tracing (MOD-006#007#008).
  • [x] Bench harness with SLO baseline (MOD-009).
  • [x] Pluggable fingerprint extractor + LSH index (MOD-010 part A).
  • [x] Pluggable classifier bridge + 24h cache (MOD-011 part A).
  • [x] Per-tenant rate limit on /v1/reports (MOD-014).
  • [x] Input validation: freetext ≤ 8 KB, evidenceurls ≤ 50 (MOD-015).
  • [x] ModerationEventSink trait — Submitted / AutoHidden / StrikeAdded / ReviewerDecision (MOD-017).
  • [x] POST /v1/classify wraps Classifier + ClassifierCache (MOD-018).
  • [x] #[instrument] on intake.submit + automod.decide_many (MOD-019).
  • [x] kdb_mod_intake_seconds + kdb_mod_strikes_active + kdb_moderation_rate_limit_drops_total metrics (MOD-020).
  • [ ] chromaprint cgo wrap (MOD-012 — blocked on libchromaprint-dev).
  • [ ] PDQ cgo wrap (MOD-013 — blocked on Meta pdq-cpp).

SLO targets

  • p99 intake.submit1 ms (non-classifier path).
  • Fingerprint match latency ≤ 30 s for 10-min upload.
  • NSFW classification ≥ 95% precision / ≥ 90% recall (MOD-011 production gate).

Bench numbers: intake.submit = 219 ns; fingerprintlookup @ 100k = 10 ms; classifiercache hit = 42 ns; automod.decide_many = 36 ns. See services/foundation/moderation/bench/baseline.json.

Interfaces

  • HTTP: POST /v1/intake, POST /v1/fingerprint/match, POST /v1/strikes, POST /v1/queue/:id/resolve (#021 reviewer decision — hidedismissescalate; hide bumps the strike ledger + emits ReviewerDecision), corpus CRUD (#022): POST /v1/corpus · GET /v1/corpus?tenant=… · DELETE /v1/corpus/:work_id?tenant=…, GET /healthz, GET /metrics.
  • Library: koder_moderation::{IntakeService, StrikeLedger, FingerprintCorpus, AutoModPolicyTable, LocalRuleClassifier, ClassifierCache, StubExtractor, LshIndex}.