Skip to content

Community moderation Phase 1: reports, bans/timeouts, audit, tombstones, relay-DM notices#1616

Merged
tlongwell-block merged 27 commits into
mainfrom
eva/community-moderation
Jul 8, 2026
Merged

Community moderation Phase 1: reports, bans/timeouts, audit, tombstones, relay-DM notices#1616
tlongwell-block merged 27 commits into
mainfrom
eva/community-moderation

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Community Moderation — Phase 1 (relay + DB + CLI)

Foundation of the moderation stack (design locked by Tyler 2026-07-07, vision + locked decisions in #buzz-moderation thread cc031bd4, plan in PLANS/COMMUNITY_MODERATION_PLAN.md).

What's here: NIP-56 report ingest (kind 1984, tenant-fenced targets), authorize_moderation_action capability helper (owner/admin via relay_members), community bans + timeouts enforced at the NIP-42 auth seam (blocked:/restricted: per NIP-01) with cluster-wide live-disconnect via Redis, NIP-OA cascade (owner ban → agents; agent ban agent-only), moderation_actions audit rows, enriched delete tombstones (40099 + action_id/reason_code/public_reason, spoof-gated), relay-signed moderation DM notices, report-queue commands (9040/9041/9042/9043/9044/9045), CLI (buzz moderation ...), schema 0006_moderation folded into schema/schema.sql, NIP-11 advertises 56.

Verification (all at head cb32adde):

  • Quinn: 8/8 live matrix at the wire — auth-seam ban, Redis fanout, timeout write-block, tombstone legs incl. member-spoof rejection, DM notices, NIP-OA cascade with controls, kick+escalate, tenant fence (action side).
  • Wren: independent second pass on items 6/8 — code-traced enforcement paths + live cross-host read-authz probes.
  • Formal model: docs/spec/MultiTenantRelay.tla MOD-1..7, mutation-tested.

Stack: this PR → eva/moderation-ui (#TBD) → #1614 (U1 member surface) → #1615 (U2 admin surface). Merge bottom-up.

Known Phase-1.5 follow-ups (non-blocking, tracked in thread): matched_principal/auth-seam audit surface, direct delete-with-reason fan-out, 9000/9001 hex-length validation, member report submission UI (Phase 2).

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 26 commits July 7, 2026 11:37
…eams

Community moderation build (design: docs/moderation/PLAN.md, decisions
locked by Tyler 2026-07-07). This commit pins the cross-lane contract:

- migrations/0006_moderation.sql: moderation_reports (NIP-56 queue),
  community_bans (ban + muted_until timeout), moderation_actions (audit).
  All tenant-scoped, community_id-leading keys per the isolation lints.
- buzz-core kinds: KIND_REPORT (1984), moderation commands 9040-9044.
- buzz-db::moderation: persistence contract (signatures + row types).
- buzz-relay handler seams: moderation_authz (capability helper),
  report (1984 ingest), moderation_commands (9040-9044 dispatch),
  moderation_notices (relay-signed DM notices).

Bodies are todo!() stubs owned by implementation lanes; signatures are
the interface. Reports are signals, never triggers (NIP-56); targets
resolve under the requesting TenantContext only.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The live-Postgres migration test hardcoded [1, 2, 3] and had rotted
(migrations 4 and 5 already exist on main; it only passes when skipped).
Derive the expected list from the embedded MIGRATOR so it stays honest
as additive migrations land — including 0006_moderation.

Verified against a fresh local Postgres: all 6 versions apply, and the
three moderation tables materialize with community-id-leading keys.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Wren's contract review of 67373d9 (two blockers + pins):

- 0006_moderation.sql: CHECK-enforce exactly-one-target-class per report
  row (target_kind authoritative, matching column only); 32-byte length
  checks on every pubkey/event-id/sha256 column; matched_principal
  restricted to self|owner; same-community FKs for channel provenance
  (reports+actions -> channels) and resolution provenance
  (reports.action_id -> moderation_actions).
- buzz-core: KIND_REPORT + 9040-9044 registered in ALL_KINDS (duplicate
  detection now covers them); is_moderation_command_kind() as the one
  canonical route check; compile-time asserts (u16 fit, not ephemeral).
- moderation_commands.rs docs: pinned 9040-9044 routing (community-global
  direct commands, is_global_only_kind, fresh timestamp, never stored,
  no channel-scoped tokens) and the exact tag vocabulary per kind so the
  CLI and relay seams cannot diverge.

Verified on live Postgres: migration applies; a two-target insert is
rejected by moderation_reports_check.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
A stray h tag must never channel-scope a moderation command; the
9040-series follows the NIP-43 9030-series model (executed, never
stored, community-global). Closes Wren's contract-review mismatch:
the note claimed this was landed at d02c9be but only the kind
constants were.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The moderation_commands.rs module doc still carried the pre-review
shape (report = row uuid, status escalated, resolution/public_reason
tags). Pinned vocabulary (thread event 86f46207): report = 1984 event
id hex, status resolved|dismissed, required action tag, optional
reason -> moderation_actions.public_reason. kind.rs doc for 9044
updated to match. No code changes.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Implements the L1 moderation persistence lane: moderation_reports/community_bans/moderation_actions DB layer, Db wrapper seams, restriction_state enforcement read, report lookup by event id, and ignored Postgres-backed invariant tests (tenant fence, expired-ban+timeout, re-ingest idempotency, double-resolve).

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
…tion seams

Phase-1 community moderation enforcement lane (L4). Consumes L1's
`Db::moderation_restriction_state` seam at three points:

- Auth seam (handlers/auth.rs): reject AUTH from a banned member with
  `OK false "blocked: you are banned from this community"` and immediately
  close the socket (decision 4). The reason frame is routed over the
  control channel and the send loop drains it ahead of the Close, so the
  client learns why before the socket drops. Includes a NIP-OA owner
  cascade — an agent whose crypto-proven owner is banned is refused too.
  A DB error fails closed but denies with `error: internal ...` rather
  than falsely claiming a ban.

- Ingest write-block (handlers/ingest.rs): a timed-out member's EVENTs
  are refused with `restricted: you are timed out until <ts>` until the
  timeout expires. A ban is also re-checked here: an already-authenticated
  connection never re-auths, so if the live-disconnect fan-out is missed
  (fire-and-forget publish, broadcast lag, subscriber reconnect window),
  this write-path gate is the durable backstop that stops a banned member
  writing indefinitely. Moderation-command and relay-admin kinds are
  exempt so the tools that lift a restriction are never disarmed. Fails
  closed. The gate checks the authoring pubkey only; the NIP-OA cascade is
  structural at the auth seam for bans, and timeout's owner cascade is a
  documented Phase-1 asymmetry (IngestAuth carries no auth tag).

- Live disconnect (state.rs): `ConnectionManager::disconnect_pubkey`
  closes every live socket for a pubkey *within the banning community*,
  delivering the close reason over the control channel. The community
  filter is the tenant fence: one pod holds sockets for many communities,
  so a ban in A must never kill the same member's session in B.

Cross-pod fan-out (buzz-pubsub/src/conn_control.rs): a new Redis pub/sub
channel carrying `ConnControl::DisconnectPubkey`, parallel to
cache_invalidation rather than folded into it — a disconnect is an
imperative, non-idempotent action, not a pure cache-key drop. main.rs
spawns the subscriber and a consumer that applies inbound commands via
disconnect_pubkey, passing the scoped community so the fence holds
cross-pod.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…pState

Adds `AppState::disconnect_pubkey_clusterwide`, the single entry point for
live ban enforcement: it closes this pod's sockets for the banned pubkey
(fenced to the community) and spawns the cross-pod `publish_conn_control`
fan-out so every other pod's subscriber closes its sockets too.

Closes the wire-up gap Quinn found: #1594 shipped both halves as separate
primitives (`conn_manager.disconnect_pubkey` + `PubSubManager::
publish_conn_control`) but nothing called the publish half, so a live ban
was pod-local-only — violating decision 4's "immediately, everywhere,
including live sessions". Rather than have the ban handler (L6) remember to
call both, pairing them on AppState (mirroring `spawn_cache_invalidation`)
makes "do half the job" unrepresentable: callers get cluster-wide
enforcement from one call and can't silently drop the fan-out.

The banning pod re-receives its own publish and no-ops (local sockets
already closed) — intentional, documented, no origin-suppression. The
return count is pod-local only; remote closes are async and unreported.
Fire-and-forget publish with the DB ban row as the durable backstop.

No dedicated unit test: pure composition of two already-tested primitives
(the fenced pod-local disconnect has three tests incl. the tenant-fence
case; publish + serde are covered in conn_control.rs). A full AppState +
live-Redis harness for a 15-line pairing is disproportionate.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…live disconnect (#1594)

* commit '47630276':
  moderation(L4): pair live-ban disconnect with cross-pod fan-out on AppState
  moderation(L4): enforce bans/timeouts at the auth, ingest, and connection seams

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Phase-1 community moderation lane L6: the moderation command dispatch and
the three HTTP read endpoints that back the mod queue.

Command handler (handlers/moderation_commands.rs), kinds 9040-9044:
- 9040 ban, 9041 unban, 9042 timeout, 9043 untimeout, 9044 resolve-report.
- Every command routes authorization through the single
  `authorize_moderation_action` capability helper (L2) — never an inline
  role check — then performs its DB write (L1), records the audit row, and
  best-effort sends the relay-signed notice DM (L5).
- Freshness gate rejects stale/replayed commands (never stored).
- 9040 ban drives live enforcement via the paired
  `AppState::disconnect_pubkey_clusterwide` (L4): one call closes this pod's
  fenced sockets and fans the disconnect out cross-pod, so a live ban takes
  effect immediately, everywhere (decision 4). The DB ban row is the durable
  backstop for a dropped fan-out.

Read endpoints (api/bridge.rs) + routes (router.rs):
- GET /moderation/reports, /moderation/audit, /moderation/restricted.
- Each mirrors the existing count_events shape: HOST-bound tenant, NIP-98
  auth + replay check, then the mod-authz gate (ViewQueue) rather than
  plain relay membership. Reads resolve tenant-scoped rows (L1) only.

Depends on L2's `authorize_moderation_action` body, still `todo!()` and
human-gated: the authz-gated paths compile against the pinned signature but
panic at runtime until L2 lands. Handler unit tests cover the pure helpers
(tag parse, expiration vocab, report-tag validation) which do not hit authz.

Validation on base 0bff742: fmt --check, check -p buzz-relay, clippy
-p buzz-relay, test -p buzz-relay --lib all green (492 passed / 0 failed;
7 L6 handler tests included). git diff --check clean. Diff is three
buzz-relay lane files only.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Two small fixes in handle_resolve from Eva's #1599 review, plain delta on
8db239b.

F1 — orphan audit row on a lost resolve race: insert_audit ran before
resolve_moderation_report, so two mods resolving the same report left an
audit row behind the failed resolve. Check the already-fetched
report.status == "open" before insert_audit and error early. The DB's
WHERE status='open' stays the real guard; a comment notes the residual
tiny window (audit row + failed resolve only) is tolerated.

F2 — resolution rows indistinguishable from enforcement rows: a one-click
resolve with action=ban wrote an audit row "ban", and the client's paired
9040 wrote a second "ban" enforcement row — double-count, decision-vs-
enforcement ambiguity. Prefix the resolution decision row `resolve:ban`,
`resolve:delete`, etc. dismiss_report and escalate stay unprefixed
(escalate must remain queryable for the platform-safety lane). Module doc
records the audit vocab.

Validation on base 0bff742 (toolchain 1.95.0): fmt --check, clippy
-p buzz-relay --all-targets -D warnings, test -p buzz-relay --lib all
green (492 passed / 0 failed / 2 ignored). git diff --check clean. Delta
is moderation_commands.rs only, +30/-2.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…nts (#1599)

* commit 'e4823614':
  moderation(L6): fix resolve-report audit races (Eva review #1599)
  moderation(L6): command handler (9040-9044) + mod-queue read endpoints

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Implements `authorize_moderation_action` (the capability helper, plan §1),
the single seam every moderation decision routes through so a future
Moderator tier is a policy change, not a rewrite (decision 1).

Authority model:
- Community owner/admin (tenant-scoped `relay_members.role`) authorize
  every action in any channel of their community — the bridge
  `validate_admin_event` is missing today.
- Channel owner/admin keep channel-local authority for DeleteMessage/Kick
  within `channel_id`. No current call site passes a `channel_id` (every
  L6 handler + the queue bridge pass `None`), so this branch is the
  contract seam for the future `validate_admin_event` wiring — building it
  to contract now makes that a one-line consult, not a helper rewrite.
- Guard rail: an admin cannot ban/timeout the community owner or a fellow
  admin; only the owner may action an admin. Scoped to ban/timeout only —
  restriction-lifting (unban/untimeout) is unguarded because a banned
  admin can't self-unban (blocked at the auth seam before any command
  runs), so the only reachable case is lifting a fellow admin's
  restriction, which is benign, audited, and owner-reversible. The guard
  trips on a target *role* of owner/admin, never on a missing row, so a
  drive-by spammer who already left is still bannable.

Tenant fence: both role reads (`get_relay_member`, `get_member_role`)
filter on `tenant.community()` in SQL — authority never crosses tenants.

The policy is factored into a pure `decide_authority` from resolved roles,
exhaustively unit-tested (7 tests: owner all-actions, admin against
non-privileged and privileged targets, non-member bannable, guard scope,
channel-role delete/kick-only, member/stranger denied). The async wrapper
is thin I/O glue that reads only the roles a given path needs.

Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
* commit 'e4e8f8c5':
  Phase-1 community moderation authorization seam (L2).

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…. query

Wren's L7 sweep (#1591) found a NIP-98 auth mismatch that 401'd
`buzz moderation reports` and `audit` in normal use: the CLI signs the
full request URL — query string included (`?limit=…&status=…`) — but the
relay reconstructed the expected URL from the bare path only, and
buzz-auth URL normalization preserves query strings, so they never
matched. `restricted` was unaffected (no query).

Fix: `authorize_moderation_read` now takes the request's raw query
(axum `RawQuery`) and appends it verbatim to the path before building the
NIP-98 expected URL, so the relay verifies against the actual request URI
the client signed. Verbatim (not re-parsed) keeps the match byte-exact
regardless of param order/encoding. `restricted` passes `None` and keeps
its bare-path expectation. This is the direction Wren recommended:
verifying the full request URL keeps future filtered reads honest.

Regression coverage (4 tests, bridge.rs): a query-bearing GET verifies
iff the expected URL carries the same query (`reports?limit=20&status=open`,
`audit?limit=20`); an anti-regression control proves the same event is
rejected with a URL mismatch against the bare path (the pre-fix behavior);
and `restricted` still verifies query-less. A test-local helper mirrors
the production query-reconstruction so the seam is pinned without a DB
harness.

Validation on base 8059413 (toolchain 1.95.0, PATH=repo bin): fmt
--check, clippy -p buzz-relay --all-targets -D warnings, test -p
buzz-relay --lib all green (503 passed / 0 failed / 2 ignored — 499 prior
+ 4 new). git diff --check clean. Delta is bridge.rs only, +157/-5.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…query (#1607)

* commit '8bbc3dae':
  moderation(L6): verify moderation reads against full request URL incl. query

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

* origin/quinn/mod-read-auth:
  moderation(L6): verify moderation reads against full request URL incl. query
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
* commit '7c83d54b':
  Route moderation ingest events

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

* origin/max/mod-l3-ingest:
  Route moderation ingest events
L6 (Quinn), CLI half. Adds the `buzz moderation` command group and the
NIP-43-style community moderation command builders it signs.

buzz-sdk builders (kinds 9040–9044), tag vocabulary pinned by
moderation_commands.rs:
- build_moderation_ban (9040): p + optional expiration + reason
- build_moderation_unban (9041): p
- build_moderation_timeout (9042): p + required expiration + optional reason
- build_moderation_untimeout (9043): p
- build_moderation_resolve_report (9044): report + status + action + optional
  reason; validates status/action vocabulary at build time

buzz-cli `moderation` group:
- Mutations (ban/unban/timeout/untimeout/resolve) sign 9040–9044 and POST
  /events, mirroring the 9030-series relay-admin transport. Community is
  host-scoped, so no channel/community flag.
- Reads (reports/restricted/audit) GET NIP-98-authed /moderation/* endpoints,
  because queue/audit rows are structured DB rows, not stored nostr events —
  serving them over a REQ filter would mean synthesizing fake events and
  threading a privileged authz check into the public read path. Read-endpoint
  server side pending Eva's lane arbitration; CLI shape is stable either way.
- client.get_authed() helper for NIP-98 GETs.

Tests: 12 SDK builder tests (tag shape, pubkey lowercasing, vocab rejection);
command-inventory stability updated for the new group. cargo check + clippy
clean; buzz-cli + buzz-sdk suites green.

Contract base: 4c58489 on eva/community-moderation.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Wren's L7 re-clear found that build_moderation_{ban,unban,timeout,
untimeout,resolve_report} validated hex targets with check_hex_len(.., 64),
which is a *minimum*-length check (built for abbreviated git SHAs). The
relay's extractors (moderation_commands.rs extract_p_tag_bytes /
extract_report_tag) require exactly 64 hex. The CLI masked this by
pre-validating with validate_hex64, but a direct SDK caller could build and
sign a p/report tag with 65+ hex chars that the relay then rejects as
missing/invalid — the SDK vouching for an event the relay drops.

Switch to the existing exact-length helpers: check_pubkey_hex for the four
target pubkeys and check_hex_exact(.., 64, ..) for the report id. Both
return the normalized lowercase value, so the redundant inline
.to_ascii_lowercase() on the tag values goes away. Error variant for
short/overlong input moves from InvalidDiffMeta to InvalidInput accordingly.

Tests: add overlong-hex regressions for a target pubkey and the report id;
update the two existing short-input tests to the InvalidInput variant.
buzz-sdk 219 / buzz-cli 135, clippy + fmt clean.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
* origin/quinn/mod-cli:
  moderation(L7): CLI commands + SDK builders for reports/bans/timeouts/audit

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>

* origin/quinn/mod-cli:
  moderation: SDK builders enforce exact-64-hex for p/report tags
  moderation: buzz-cli moderation command group + SDK 9040–9044 builders
* origin/main:
  docs(readme): add Getting started section routing install paths (#1606)
  fix(desktop): restrict shared-agent sync to dev data dirs (#1597)
  feat(desktop): restart-required badge from spawn-time config hash (#1602)
  feat(desktop): boot-time reconcile of managed agents to relay events (#1601)
  feat(desktop): canonical <PubKey> component — hover to view/copy full keys, owner "you" labels (#1589)
  fix(desktop): hydrate reactions for Inbox context messages (#1596)
  fix: cleanup old screenshots that my agents committed (#1598)
  chore(release): release Buzz Desktop version 0.3.46 (#1585)
  fix(desktop): preserve agent model/provider when persona snapshot fields are blank (#1583)
  feat(acp,desktop): identify and reap stale agent harness processes (#1582)
  feat(desktop): active-draft badge, send-from-drafts confirm dialog, thread-deleted state (#1581)
  fix(desktop): treat baked build env vars as satisfying required agent config (#1580)
  feat(desktop): add "Copy image" to image right-click context menu (#1579)
  fix(nest): use buzz-dev symlink name for dev builds (#1587)
  fix(composer): address image-editor follow-up nits on #1491 (#1565)
  fix(desktop): render black static boot screen (#1570)
  feat(agents): group activity tool bursts (#1571)
  feat(desktop): aggregated overview rail, commit detail page, and full breadcrumbs (#1573)
  fix(desktop): fetch profiles for reaction actors and thread-reply authors (#1550)

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
The pgschema provisioning path (scripts/start-*-relay.sh, fresh-DB CI)
applies schema/schema.sql only and is blind to migrations/. Without this
fold, freshly provisioned DBs lack moderation_reports/community_bans/
moderation_actions and kind-1984 ingest 500s (hit live during the Phase-1
demo). Verbatim body of migrations/0006_moderation.sql; verified by
pgschema apply on a fresh DB + zero-diff pgschema plan against an
AUTO_MIGRATE-built DB.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
kind:1984 report ingest is live since the Phase-1 moderation merge but
the NIP-11 document didn't advertise it, so interop clients wouldn't
discover report support (Quinn's e2e cap note). Adds 56 to
SUPPORTED_NIPS with a mirror test.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
The Phase-1 community-moderation stack is built and reviewed; the
working plan document has served its purpose. Replace it with a
root-level vision doc in the register of the sibling VISION_*.md
files: the report -> queue -> decide -> enforce -> audit -> notice
loop, the community/platform two-layer split, and the honest edges
(escalation is a hook, two roles, best-effort notices, no automod).
Link it from VISION.md alongside the other sub-visions.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block tlongwell-block merged commit 863aeb7 into main Jul 8, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant