A community based topic aggregation platform built on atproto

fix(ingestion): close the handle-collision flood and sort the fetch taxonomy master

Four fixes for RED's pins at fa38074. Three of them are one causal chain that ended as a dead-letter flood on a lane four layers from the cause. 1. communities: CreateCommunity's profile record now carries `handle`. That record is the ONLY thing that tells an AppView a community exists, so omitting the field left the consumer resolving one from the DID document — which on an egress-blocked stack yields "handle.invalid". Writing it is not a departure from "handles are mutable, resolve from DIDs": that guidance is about trusting a STRANGER's self-reported handle, and this process provisioned the account and asked the PDS for exactly this one. Federated communities still resolve, because there resolution is the only option. 2. jetstream: the community consumer no longer stores an unverifiable handle. Identity resolution reports an unverifiable DID by returning the reserved "handle.invalid" rather than an error, so a well-formed identity naming a non-handle was landing in a UNIQUE column — and the first one to do so made every later unverifiable community collide with it. TRANSIENT, because it is a fact about the resolution and not about the record: the directory may answer fine a minute later. Mirrors the user path's guard in authorpost.go. 3. jetstream: the conflict swallow is narrowed. communities.IsConflict matches two errors that mean opposite things — ErrCommunityAlreadyExists IS an idempotent replay (walked constantly, must stay a silent no-op), while ErrHandleTaken means a DIFFERENT DID holds the handle and the community in the event was never indexed at all. The second is now a PERMANENT refusal naming both DIDs and the handle; an unclassified conflict is reported rather than swallowed, so a future unique constraint cannot inherit the same silence. 4. jetstream: DirectPostFetcher sorts a non-200 getRecord instead of reporting one undifferentiated failure. A genuine XRPC RecordNotFound is permanent — the PDS was reached and answered, and left transient any community could mint unlimited lane-blocking by writing acceptances for URIs nobody wrote. A BARE 404 stays transient: with no envelope the request most likely never reached a PDS, so trusting it would discard a real post over a mistyped hostname. 5xx stays transient by definition. The predicate is EXPORTED from users (IsRecordNotFoundResponse) and FetchProfileRecord now calls it, so the line is drawn once. Why permanence is the load-bearing half of 3 and 4: a transient error costs the connector three inline retries — about 4.2 seconds of a blocked lane that now carries four collections — plus ten redrives, per delivery. ErrPermanentEvent short-circuits all of it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


+156 -13
4 changed files