A community based topic aggregation platform built on atproto

feat(bridgedvotes): poll the bridge vote-aggregate side channel for native content master

Bridge-asserted vote tallies used to reach the AppView only as bridgedStats stamped onto records via Jetstream, a channel that cannot touch content a native user authored in their own PDS repo. Votes cast on Lemmy against native posts and comments were therefore aggregated by the bridge and displayed by nobody. This adds a background poller that sweeps the trusted bridges' social.coves.bridge.getVoteAggregates endpoint for content in bridged communities and folds the fediverse tallies into the existing bridged_upvote_count / bridged_downvote_count / bridged_stats_as_of columns under the same >= asOf guard the Jetstream path uses. The invariant the design is built around: a value from the database (communities.pds_url) is never a dial target. Stored URLs are match keys only; the dial target is always the matched operator-configured host from TRUSTED_BRIDGE_PDS_HOSTS, and that is now a type (TrustedHost) that only ParseTrustedHost can produce. Changes: - internal/core/bridgedvotes: new package. Client (SSRF-guarded default, redirect refusal, 1 MiB cap, request-set binding, first-wins dedupe, transient vs permanent classification with 408/425/429/5xx transient, missing-key and all-rejected responses as contract errors). Poller.Sweep groups candidates per trusted host with per-host budgets, marks permanently failed batches past the rotation, bounds transient streaks at three sweeps so a persistent 5xx cannot wedge a host, isolates selection failures per host, and returns a Report the job logs. Shared NormalizeHost backs both trust matching and routing; shared ParseAsOf rejects the zero time and stamps more than five minutes ahead of the AppView clock, in both ingestion channels. MaxBridgedCount is exported and the Jetstream adapter aliases it. - internal/db/postgres: BridgedVotesRepository with candidate selection across posts UNION ALL comments (comments qualify only via a non-deleted indexed root), guarded ApplyAggregate with score recompute in one UPDATE, ErrMissingAsOf for a zero stamp, transactional MarkPolled. - Migration 043: bridged_polled_at watermark columns and partial rotation indexes on posts and comments; corrects 031's column comments to the >= semantics actually implemented, with a symmetric Down. - internal/config: BRIDGED_VOTE_POLL_INTERVAL / _LOOKBACK / _SWEEP_CAP; Validate rejects trusted-host entries that are not scheme+host, a non-positive interval when a trust list is set, and negative tuning. - cmd/server: buildBridgedVotePoller under the same non-empty trust guard as BridgeTrust, using the guarded client with the dev private-host hatch; startBridgedVotePollJob logs a report per sweep and warns when stored community hosts match nothing in the trust list. - docker-compose.prod.yml / .env.prod.example: pass-through and docs for the new variables, including per-host cap semantics and the fail-fast contract for malformed trust entries. - Tests: T0 coverage for the client, poller, normalizer, TrustedHost, ParseAsOf, config parsing and validation, job guard, and server wiring; T1 coverage for the repository, migration rollback chains, and an end-to-end sweep through the real repository, client, and an httptest bridge. Verified: make test, integration tier for touched packages, test-audit and ssrf-audit clean. Deferred: EXPLAIN ANALYZE the two partial indexes on production-shaped data (the ORDER BY runs after the UNION, so they may not serve the query); ApplyAggregate still probes posts then comments rather than splitting on the URI collection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Jd4QsKtSPJRrMc83ayyqD