Repository navigation
v1.4.0 — Account moderation state model
[1.4.0] - 2026-04-26
v1.4 "Account moderation state model" turns moderation into
first-class records: every action against a subject (warning,
note, suspension, takedown) writes a structured row with a
strike value resolved at action time and frozen for forensic
durability. Strikes accumulate, dampen for first-time offenders,
and decay over time per operator-configurable rules. Read
endpoints — admin and a new user-facingtools.cairn.public.*
namespace — recompute strike state through a pure decay
calculator on every fetch, so cached values can never produce a
misleading answer. Operators declare reason vocabularies and
strike policy in[moderation_reasons]and[strike_policy]
config blocks; the new §4.2 disclosure 4 makes the trade-off
explicit: cairn-mod's contribution is making policy declarable
and observable, not adjudicating what the policy should be.
Added
- Account moderation state model:
subject_actionstable records every moderation action (warning, note, temp_suspension, indef_suspension, takedown) with structured reason metadata, duration, notes, and links to source reports.subject_strike_statecache table tracks current strike counts per subject_did. Append-only schema; revocation transitions are the only allowed UPDATE per the trigger contract from §F20.6 (#46) - Reason vocabulary system: operators declare moderation reasons in
[moderation_reasons]config block withbase_weight,severeflag, anddescription. Cairn-mod ships eight default reasons aligned with ATProto'sreasonType(hate-speech, harassment, threats-of-violence, csam, spam, misinformation, nsfw, other). Operator-declared blocks replace defaults entirely (no merging) per §F20.2 (#47) - Strike policy system:
[strike_policy]config block declaresgood_standing_threshold(default 3),dampening_curve(default[1, 2]),decay_function(linear or exponential),decay_window_days(default 90),suspension_freezes_decay(defaulttrue), andcache_freshness_window_seconds(default 3600). Per-field defaults let operators declare partial blocks per §F20.3 (#48, #55) - Strike calculator: pure function applies dampening at action time. Users in good standing get curve-position values; users out of good standing get full
base_weight; severe reasons bypass dampening. Thewas_dampenedflag andstrikes_at_time_of_actionare frozen on the row for forensic auditability per §F20.3 (#49) - Decay calculator: time-based decay computed on read, not stored. Linear decay reaches 0 at
decay_window_days; exponential decay reaches ~1% at the same boundary (half-life = window / log₂(100)). Suspension freezes decay (v1.4 simplification: only the most recent unrevoked suspension affects calculation) per §F20.4 (#50) - Recorder + revoker:
WriteCommand::RecordActionandWriteCommand::RevokeActionroute action writes through the writer task. Single-transaction atomicity acrosssubject_actionsrow,subject_strike_statecache update, and hash-chainedaudit_logrow via #39's pathway. Predict-then-verify pattern on thesubject_actions.idensuresaudit_log_idlinkage stays correct even under sequence-allocation edge cases (#51) - Position-in-window calculator: pure function counts in-good-standing offenses within the current decay window. Uses each prior action's
was_dampenedflag as the "in good standing at its time" predicate so position counting is stable across policy edits (#51) - Multi-reason resolver: when an action carries multiple reason codes, the strike calculation uses the dominant reason — severe wins regardless of
base_weight; ties onbase_weightresolve to first-listed deterministically (#51) - Admin XRPC:
tools.cairn.admin.recordAction,tools.cairn.admin.revokeAction(writes);tools.cairn.admin.getSubjectHistory,tools.cairn.admin.getSubjectStrikes(reads). All Mod-or-Admin authorization. The sharedsrc/server/strike_state.rsmodule factors the projection logic used by both admin and public read endpoints (#51, #52, #53) - Public XRPC:
tools.cairn.public.getMyStrikeState. First endpoint in thetools.cairn.public.*namespace. Service-auth gated; the verifiedissmust equal the subject_did. CORS allows browser-side callers (the namespace is designed for downstream consumers like future accessory bots or Web UIs). Cross-references admin'ssubjectStrikeStatetype to avoid type drift (#54) - Operator CLIs:
cairn moderator action/warn/note/revoke/history/strikes— moderator-tier, HTTP-routed via admin XRPC, cursor-paginated history, structured strikes display with decay trajectory. ThedecayWindowRemainingDaysfield is omitted at zero strikes since trajectory is meaningless without strikes to project (#51, #52) - Subject-strike-state cache management:
cache_is_freshpredicate andget_or_recompute_strike_countentry point. Cache bypass is the v1.4 read-endpoint invariant; the cache exists for v1.5+ consumers needing O(1) "is this user in good standing?" reads. Best-effort cache writes during recompute (write failure logs but doesn't fail the read) per §F20.9 (#55)
Changed
cairn-design.mdgains §F20 (account moderation state model), ten subsections covering action types, reasons, strike calculation, decay, revocation, schema, XRPC surface, operator CLIs, cache management, and deferred future work. §4.2 trust-chain disclosure 4 documents that operators set their own moderation policy and that policy declarability is cairn-mod's contribution rather than a fixed moderation philosophy. §18 roadmap updated to mark v1.4 as shipped and surface the deferred-to-future-releases items from §F20.10 (#56)cairn-design.md§F10 audit-log action vocabulary gainedsubject_action_recordedandsubject_action_revokedentries (lexicondefs.jsonknownValues+AUDIT_ACTION_VALUES+ design-doc prose). Update landed with #51's commit since the recorder writes those actions (#51)