feat(moderation): make a community's removal real and terminal (17c-1) master
Unlocks inbound moderation of native content, and fixes what unlocking it would otherwise have caused. mapping.CommunityDID is now populated: it rides consume.PostIntent and CommentIntent (both producers already hold the resolved community, so the Enqueuer grows no dependency) and migration 024 backfills existing rows by an EXACT join against outbound_objects rather than a derivation — a wrong binding would let one community's moderators remove another's content, which is worse than none. putMapping's upsert now COALESCEs the column so a later write that omits it cannot NULL a good binding and disable moderation forever. That column is what authorizes announced moderation (decision 18) — and it also gates announced VOTES and removals on native posts, which failed subjectBelongsToCommunity and authorizeAnnouncedContentDelete for want of it. The post-removal path itself was already built (materialize.RemovePost/ RestorePost) and merely unreachable. TERMINALITY: the engine's auto-restore caught acceptrec's commit-time removal guard and reversed it unconditionally. That is correct for our own admission-revoked decisions, which a corrective edit should undo — but once a moderator-discretion removal can exist for a native post, the next author edit deleted the moderator's removal, wrote a fresh acceptance and enqueued an Update{Page} BACK AT THE COMMUNITY THAT REMOVED IT. The restore now reads the standing removal's code first: only admission-revoked reverses; anything else records a moderator-removed ledger row, enqueues nothing, and lets the removal stand. A removal that vanished between the refusal and the read errors so the event retries rather than being treated as terminal; an unreadable code is treated as a moderator's, because refusing an edit is recoverable and pushing a removed post outward is not. Boomerang suppression stays STRUCTURAL: the materializer's RemovePost uses ApplyOps and has no TxSideEffect parameter, so it cannot enqueue. Nothing here calls acceptrec.Remove, which does. Two defects the column exposed, fixed in the same commit: - resolveSubject silently switched branches once community_did was set, onto a path with no outbound-tombstone check — replies to and votes on an author-deleted native post would have started federating again. The check is folded into the existing read (one row: a second read could see a delete land between them). - RestorePost was a silent no-op for native posts: it read the CID from mapping.DID, which is the AUTHOR's repo and not one we host, so every native restore logged "no record to re-accept". The pin source now dispatches on origin; a tombstoned outbound row refuses the restore, because an acceptance pinning a deleted record is worse than none. Two co-hosted communities in the fixture from the first cycle: decision 18's rule is a conjunction (signer == community AND target in community) and a one-community fixture cannot tell it from a broken one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>