From 286f25364e30349ceeaf7e279c6bd5fe4afb20c7 Mon Sep 17 00:00:00 2001 From: Bretton Date: Sat, 8 Aug 2026 11:51:38 -0700 Subject: [PATCH] =?UTF-8?q?chore:=20retire=20the=20build-loop=20trackers;?= =?UTF-8?q?=20fold=20durable=20backlog=20into=20PRD=20=C2=A712?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The author-owned-posts loop is complete (8 tasks, merged). plan.md and loop_state.md were the loop's throwaway coordination artifacts (512e00e precedent); durable follow-ups now live in PRD §11 (deploy runbook) + §12 (backlog). Co-Authored-By: Claude Fable 5 --- docs/PRD_AUTHOR_OWNED_POSTS.md | 40 ++++++ loop_state.md | 233 --------------------------------- plan.md | 204 ----------------------------- 3 files changed, 40 insertions(+), 437 deletions(-) delete mode 100644 loop_state.md delete mode 100644 plan.md diff --git a/docs/PRD_AUTHOR_OWNED_POSTS.md b/docs/PRD_AUTHOR_OWNED_POSTS.md index 0dbf237..c96756d 100644 --- a/docs/PRD_AUTHOR_OWNED_POSTS.md +++ b/docs/PRD_AUTHOR_OWNED_POSTS.md @@ -757,3 +757,43 @@ already excludes non-accepted; no leak). Neither blocks the merge. **Merge hygiene:** never run `go mod tidy` (upgrades transitives into a broken go-log; go-car is pinned indirect — `make ci` uses `mod download`, safe). + +--- + +## 12. Post-loop backlog (folded from the build-loop tracker, 2026-08-08) + +The 8-task build loop is complete and merged. Durable follow-ups that outlived +the loop's throwaway tracker (file as issues; none blocks the current feature): + +- **Legacy drain follow-up (§11, gated):** remove the deprecated + `social.coves.community.post` consumer/WantedCollections/read surfaces only + after the prod re-materialization run confirms zero remaining records AND + zero fallbacks. The read-side collection-aware branches, `blobOwnerOf` + community fallback, `legacyPostCollection`, and `applyRemoval`'s absent + collection guard retire together in that branch. +- **Fingerprint retype to `PostV2Record`:** deploy-sequenced to the + writers-stopped maintenance window (`TRUNCATE post_submissions` alongside) — + a live retype strands in-flight dedupe reservations. Ripples through + `enhanceExternalEmbed`/`postV2From`. +- **`community.post_count` incrementer:** wire onto + `countAcceptedPostsForCommunity` (increment on →accepted, decrement on + accepted→removed/rejected in the admission consumer). Cosmetic — display + already excludes non-accepted; no leak. +- **Orphan `community_post_admissions` sweep on community deletion.** +- **Ingestion-lane abuse hardening:** per-source-DID token bucket on the + shared posts consumer (remaining ~4.2s transient-retry stall on + unknown-community events); handle-squatting is now loud (dead-letter) but not + closed (a DID can still front-run a community's desired handle). +- **Harness:** `-race` full-suite run exhausts Postgres connections — batch it + if a race tier ever joins `make ci`; the reliability suite recreates the + appview container late, so `.ci-out/appview.log` loses the early-run window + (capture continuously). Never `go mod tidy` (breaks a go-log transitive; + go-car is pinned indirect). +- **Deferred product/UX:** author-self-view on `post.get` (needs a viewer-aware + `GetViewsByURIs`; author reaches own posts via `actor.getPosts` + `getStatus` + today); self-hoster SSRF allowance for private-address unfurl targets + (`IS_DEV_ENV` is the only current escape hatch). + +**Open product question for the owner:** comments bypass admission entirely — a +banned author can still comment (PRD open question #2). Decide whether bans +should gate comments before Beta. diff --git a/loop_state.md b/loop_state.md deleted file mode 100644 index 8a12060..0000000 --- a/loop_state.md +++ /dev/null @@ -1,233 +0,0 @@ -# Author-owned posts build-loop state - -Protocol: `plan.md` §Loop protocol — one task per iteration: -analyze (parent) → **/tdd** (Opus 5 phase agents — never downgrade) → -**/second-opinion** (base: `feat/author-owned-posts`) → **/fix-pr** → -`make ci` → `--no-ff` merge into `feat/author-owned-posts` → delete task -branch/worktree → update this file → next task. -Spec authority: `docs/PRD_AUTHOR_OWNED_POSTS.md` (rev 2.1). -`main` is touched once, at the end, via /merge-to-main after task 8. -Stop when every task is done, or on any `blocked:` row. - -## Task table - -| # | Task | Phase | Status | Merge commit | Notes | -|---|------|-------|--------|--------------|-------| -| 1 | Lexicons: postv2 + acceptance + removal, deprecation note, fixtures, T0 validation | A | done | 4574151 | make ci 4640/0. 10-stream review; codex HIGH: rkey transform non-total → digest scheme (PRD rev 2.2) | -| 2 | Migration 034: community_post_admissions + posts FK drop; admissions repo + T1 transition/watermark matrix | A | done | 9491744 | make ci 4735/0. PRD → rev 2.4 (tuple watermark, pending-only rejection CAS, repo-side op-rank). Behavior flips: unknown authors indexable, explicit deletion sweep | -| 3 | admitPost extraction + NEW policy (bans, rate limits, dedupe) wired into existing write path | A | done | df97cb2 | make ci 4840/0. PRD → rev 2.6. Migration 035 ledger (plan review killed posts-table limiter). BANS NOW ENFORCED. T2 wire probe added | -| 4 | Acceptance engine: deterministic rkey, swap-safe acceptance writer, atomic applyWrites removal, repin/terminality rules | B | done | e00d97a | make ci 4944/0. Probe-driven plan review killed 4 assumptions pre-code. 2 production bugs fixed as side effects. Gate saga: 2 latent test defects fixed + Docker restart (150d uptime) | -| 5 | Ingestion: postv2/acceptance/removal consumers, watermark gating, direct-fetch convergence, WantedCollections + 3 e2e contracts | B | done | 0caeda4 | make ci 5047/0. PRD → rev 2.7. getStatus pulled forward; migration 036; queue driver + decider + factory; 6 production defects fixed as by-catch; CAR-recomputed CID verification | -| 6 | Write path flip: author-repo postv2 via session, author-PDS blobs, sync fast-path accept, post.delete flip, post.update NEW | C | done | 2c66287 | make ci 5120/0. pragma:security FIRED — bypass CLEAN. Seed-rewind (published false acceptance) + UpdatePost validation bypass + dead token step + SSRF fixed. DEPLOY GATE: not ahead of task 7 | -| 7 | Read path: centralized visibility predicate, full surface inventory, #removedPost, getStatus, alternate-endpoint invisibility T2s | C | done | 4780d36 | make ci 5157/0. PRD → rev 2.8. SECURITY SIGN-OFF: 6+7 deploy-safe. 2 live leaks caught+fixed (removed-legacy via getComments; accepted-CID edit window). Deploy gate CLEARED | -| 8 | Cutover: re-materialization script (ledger, verify-before-delete, hermetic test), old-path removal, docs, tracker cleanup; panel on whole branch; /merge-to-main | D | pending | | prod script run is MANUAL, outside loop | - -## Cross-iteration notes - -- (2026-08-07, task 1) **rkey design changed under review**: acceptance/removal - rkeys are SHA-256 → unpadded lowercase base32 digests of the subject AT-URI - (PRD rev 2.2) — the readable transform broke on >512-byte / percent-escaped - DIDs. TASK 4 MUST implement the digest helper with long-DID and - percent-escape test vectors. -- (task 1) Fixture harness parity: fixtures containing blobs need - atdata.Blob conversion before ValidateRecord (both harnesses now do this — - convertBlobs in tests/lexicon_fixtures_test.go and cmd/validate-lexicon). -- (task 1) validate-lexicon's coverage report is now honest (parses - defs.main.type): 4 pre-existing record types have zero fixtures - (actor.block, community.block, aggregator.authorization, - aggregator.service) — pre-existing gap, not this loop's scope. -- (task 1) One transient unreproducible `make test` FAIL observed after the - fix batch (no package captured; cold-cache ×2 green; make ci 4640/0 - green). Watch for recurrence — if seen again, capture the package and - /file-issue. -- (2026-08-08, task 2 incident) **Conductor wiped uncommitted GREEN work**: - `git checkout ` used to revert a deliberate test-bite mutation reset - admission_repo.go to the RED-stub commit because GREEN's gate-passed - cycle-1 work was never committed. Recovered from the persistent GREEN - agent's context. HARD RULES now: (1) commit at EVERY gate — RED gate AND - GREEN gate, before the next phase starts; (2) revert deliberate mutations - by re-editing the line, NEVER `git checkout`/`git restore` on files with - uncommitted multi-agent work. -- (2026-08-08, task 2 → TASK 5 OBLIGATIONS): the engine/consumers must use - the AdmissionRepository outcome taxonomy correctly (skips NEVER - dead-letter); rejection = RecordRejection(judgedCID) from pending only — - re-acceptance failure is REMOVAL not rejection (PRD §5.6); ingestion must - gate events for DELETED accounts (stale replay could recreate swept - admissions — codex catch, deferred); community deletion leaves orphan - admissions rows (pre-existing-adjacent, sweep in task 5 or 8). -- (task 2 → TASK 7 OBLIGATION): post read paths INNER JOIN users — posts by - unknown authors index fine but are INVISIBLE to every hydrating read - (GetViewsByURIs/GetByAuthor). Task 7's visibility work must add - opportunistic-hydration-tolerant joins or the write path's promise breaks - silently at the read path. -- (2026-08-08, task 3 → OBLIGATIONS/DECISIONS): TASK 5 must reuse admitPost - as the engine's decision core (it returns undecided on infra failures - precisely so codes are never persisted for outages) and must gate events - from deleted accounts. TASK 6: derive postv2 rkeys deterministically from - the submission fingerprint so PDS retries become idempotent (closes the - lost-response duplicate asymmetry documented in §4.2); the write-path - flip re-triggers the bypass security review. TASK 7 unchanged obligations. - PRODUCT QUESTION for Bretton: comments bypass admission entirely — a - banned author can still comment (PRD open question #2); decide whether - bans should gate comments before Beta. -- (2026-08-08, task 4) RESOLVED in-task, no issue needed: comment edit's - dead swap-conflict handling FIXED (ErrSwapConflict || ErrConflict) and - the retried-delete-500 defect FIXED (RecordNotFound name mapping; the - self-retiring pinned test fired exactly as designed and was retired). -- (task 4 → TASK 5 OBLIGATIONS, additive to task-2/3 lists): wire - social.coves.community.acceptance + removal into consumerWantedCollections - FIRST — the engine's catch-up stamp covers the stranded-pending hole but - the firehose consumer is still the authority; drive the engine via a - LEASELESS queue (safe only because every write is idempotent — documented - in engine.go); serialize the queue per community DID (swapCommit is - repo-global — sibling workers on one busy community starve removals); - classifyRecordDiff ships PURE+UNCALLED — task 5 invokes it with old/new - event snapshots AND applies the §5.5 bridge-trust gate; §8 edit-debounce - belongs to the queue driver; no production AdmissionDecider/ - CommunityRepoFactory exists yet — task 5/6 wire them, factory MUST fail - closed on unhosted communities (DID-mismatch guard is tested). -- (task 4, suite-health findings fixed at root): invalid lexicon fixtures - must carry EXACTLY ONE violation (map-order coin-flip otherwise — - tribunal-vote fixture repaired); T2 waits that can legitimately run long - need poll cadences whose 100/min-bucket arithmetic outlasts - contractBudget (comment contract's parent-post wait moved to 600ms after - 5 consecutive cliff failures at 23.97s-measured healthy latency). WATCH: - posts-consumer drain latency under full-suite parallel load is ~20s+ — - if another contract trips the cliff, revisit contractPollInterval - systemically and profile the consumer (backlog candidate). -- (task 4 locked decisions): applyWrites writers are STATE-SHAPED (no - upsert/tolerant-delete in the PDS — read both rkeys, shape create/update/ - delete per presence, swapCommit-guard the read-then-write); validate:false - on applyWrites (unpublished lexicons fail validate:true); acceptance - re-fire must not mint a new CID (skip-if-already-pinned / reuse - createdAt); engine = ProcessAdmission one-row contract, no lease (safe - ONLY because every write is idempotent — documented); optimistic - ApplyAcceptance with commit rev = optimization, firehose is authority, - own-echo skipped_stale = success; diff classification ships PURE + - UNCALLED in task 4 — TASK 5 invokes it with old/new event snapshots and - drives the engine via a leaseless queue. -- (task 3, backlog candidates — /file-issue if they survive the loop): - registered-aggregator limiter is fail-open (RecordAggregatorPost failures - logged-only, non-atomic count) — pre-existing; IsAggregator lookup - failure downgrades to user class (documented, stricter-path fallback); - post_submissions + aggregator_posts sweeper/retention (§8). -- (2026-08-08, task 5 → DEFERRED DECISION, owner input wanted): the §5.5 - bridgedStats REPIN path is wired nowhere — classifyRecordDiff and - RepinAcceptedCID ship pure+uncalled. Consequence: once bridges write - postv2, every stats refresh strobes the accepted post through - pending_reacceptance + a fresh acceptance record (the exact churn §5.5 - exists to prevent). SAFE TODAY: no postv2 bridged traffic exists until - tidepool adopts postv2 (post-cutover). Blocker for wiring it: the - consumer keeps no old-record snapshot, and reconstructing from columns - is lossy in exactly the way the classifier's fail-closed doc forbids — - needs a design decision (stored record snapshot vs PRD open question #3's - separate bridged-stats record, which would dissolve the problem - entirely). DECIDE BEFORE tidepool's postv2 migration; candidate homes: - task 6 (touches record shapes) or a named follow-up with open question - #3. Also carried: credential force-renew gap (revoked tokens defer - forever — needs a communities.Service method); removal-DELETE events are - logged no-ops (pair-PUT-outranks argument, review-flagged). -- (2026-08-08, task 5 gate saga — ROOT CAUSE, evidence-backed): the e2e - starvation flood = TWO composing PRE-EXISTING production defects, fixed - in-task: CreateCommunity omitted `handle` from the profile record - (egress-blocked PLC lookup → handle.invalid → UNIQUE collision) and the - community consumer swallowed ErrHandleTaken as idempotent-replay → - T1-created communities silently dropped → 72 of their records - dead-lettered transiently at 4.2s inline head-of-line blocking each ≈ - 5min posts-lane outage spanning T2. The defect was ALREADY DOCUMENTED - in community_contract_test.go:81-95 as reported-not-worked-around. - Task 5 tripled the poison (4 collections on one lane), making a - marginal latent failure deterministic. Relay/cursors/load exonerated. - FORENSIC GAP filed for suite health: the reliability suite recreates - the appview container late in make ci, so .ci-out/appview.log loses - the entire early-run window — capture continuously (backlog candidate). -- (2026-08-08, deps): sync.getRecord CAR verification pulled in - github.com/ipld/go-car pinned to indigo's exact version WITHOUT `go mod - tidy` — tidy upgrades transitives into a broken github.com/ipfs/go-log. - Do not tidy this module until that upstream resolves; note for task 8's - cleanup pass. -- (2026-08-08, task 6 → HARD DEPLOY GATE, security-mandated): the write - flip REMOVES the credential barrier that gated who could put a post into a - community (that IS the feature). Pre-task-7 reads are status-agnostic, so - on this branch any authenticated user can write a postv2 naming ANY - community (banned-from/private/unrelated) and it renders as that - community's content immediately, regardless of the engine's acceptance - decision. CLOSED completely by task 7's visibility predicate. Loop-safe - (whole branch merges to main once, at task 8 — never task-6-alone). BUT: - task 6 MUST NOT deploy to prod ahead of task 7. Bypass enumeration itself - is CLEAN — the firehose path independently re-derives full admission + - quota for any direct-PDS write; no gate is bypassable. -- (task 6, deferred to task 8 with pointers): fingerprint retype to - PostV2Record (byte-stability makes re-materialization the free moment); - the community.post delete branch + blobOwnerOf community-fallback - scaffolding; RecordAggregatorPost double-meter on converged retry - (backlog); §8 edit-debounce now reachable at volume via the new update - path (backlog, task-4-originated). -- (2026-08-08, task 6 → TASK 7 OBLIGATIONS, now with security teeth): the - centralized visibility predicate must gate EVERY read surface on - community_post_admissions (security §6: post_repo.go + all feed queries - currently reference it NOWHERE outside getStatus) — this is the - compensating control for the deploy gate, not just a feature. Also: - blob_transform's blobOwnerOf per-record owner is LIVE (postv2→author, - legacy→community) — task 7's hydration must honor it, and AuthorView.PDSURL - is now carried out of the scan (was dropped). getStatus↔post.get admission - convergence per task-5 note. SELF-HOSTER config surface (GREEN flag): the - SSRF fix silently drops thumbnails whose unfurl targets resolve to private - addresses (internal wikis / same-network services); IS_DEV_ENV is the only - escape hatch — a narrower per-host allowance is a deliberate config-design - task if self-hosters need it (backlog, owner decision). -- (2026-08-08, task 7 → TASK 8 OBLIGATIONS): community.post_count STORED - column incrementer is unwired — countAcceptedPostsForCommunity is the - accepted-only source of truth the task-8 consumer follow-up must converge - the column onto (increment on →accepted, decrement on accepted→removed/ - rejected); no leak meanwhile (display queries exclude non-accepted). - applyRemoval has NO collection guard (a legacy post can carry a removed - admission row — intended, moderators can remove legacy posts; the read - path now honors it). The deprecated community.post collection + its - consumer + blobOwnerOf community-fallback + the fingerprint retype all - drain/retire in task 8. Author-self-view on post.get deferred (GetViewsByURIs - stays 2-arg; author reaches own via actor.getPosts+getStatus) — reconsider - if the interface change earns its keep. -- (2026-08-08, CONDUCTOR DISCIPLINE, slipped TWICE in task 7): commit GREEN's - gate-passed work BEFORE dispatching the next phase or running make ci — an - agent reporting "ci passed" is NOT a commit (ci.sh snapshots the working - tree; the commit is separate). Both times recovered (nothing lost — the - tree held it), but the every-gate rule means COMMIT AT THE GATE, then - proceed. -- (harness, multi-agent stacks): ONE coves-ci compose-project runner at a - time — a conductor ci run and an agent test-e2e run collided (force- - recreate mid-run → phantom dead-letter floods + starved lanes). The - conductor owns stack runs; agents request them. -- (2026-08-08, task 5 → TASK 6/7/8 OBLIGATIONS): TASK 6 — write-path flip - re-runs the bypass security review; deterministic client-chosen rkeys - from the submission fingerprint (idempotent PDS retries); the old - community.post author-not-found transient burn decision; sync fast path - 'pushes work at the engine' claims become true here. TASK 7 — getStatus - ↔ post.get admission-context convergence (getStatus rationale notes the - hydrating-join blindspot); §9's re-scoped T2 arcs land with the read - paths. TASK 8 — drop deprecated community.post collection + its marker; - orphan community-admissions sweep (re-deferred from task 2/5); go mod - tidy hazard; forensic appview-log continuous capture (backlog). - BACKLOG (owner-visible): per-source-DID token bucket on the ingestion - lane (remaining ~4.2s transient stall primitives: unknown-community - events); handle-squatting primitive now LOUD but not closed; -race - suite needs batching if it ever joins the gate. -- (harness) pr-review-toolkit agents unregistered this session → - /second-opinion runs general-purpose stand-ins with specialty briefs - (worked well). Named TDD agents spawn in mailbox mode — gate on their - idle notification, and commit RED's work at the RED gate so GREEN - tampering is mechanically diffable (mtime check used in task 1). - -- (2026-08-07) Loop scaffolded. Owner decisions locked in PRD rev 2.1: - new NSID `social.coves.community.postv2` (not feed.post — hierarchy kept - deliberately); `post.notify` + service auth deferred to Beta follow-up; - lexicons are PUBLISHED — never edit `community.post`'s schema, deprecation - note only. -- /tdd model check (2026-08-07): skill default is opus (= Opus 5 here); - `sonnet`/`haiku` exist only as explicit downgrade modifiers — never pass - them. -- Known adjacent defect (pre-existing, do not fix inline): community blocks - indexed but never enforced (issue 2026-07-29) — task 7's inventory will - touch the read paths where this surfaces; keep it filed, don't scope-creep. -- Kagi/aggregator posts are the bulk of prod data; task 8's script leans on - the aggregator's stored OAuth session (migration 025) for re-authoring. diff --git a/plan.md b/plan.md deleted file mode 100644 index 715f168..0000000 --- a/plan.md +++ /dev/null @@ -1,204 +0,0 @@ -# Author-owned posts — build-loop plan - -**Spec: `docs/PRD_AUTHOR_OWNED_POSTS.md` (rev 2.1). This file is the -implementation partition + loop protocol; the PRD is the design authority. -Where they disagree, the PRD wins — fix the PRD first, then the code.** - -Tracker: `loop_state.md` (living state, one row per task). - -## Branch strategy - -One **integration branch**, `feat/author-owned-posts`, cut from `main` at -loop start. Each task runs on its own short-lived branch cut from the -*current integration head* (the /tdd worktree provides this isolation), -and folds back in at iteration end: - -``` -main ──────────────────────────────────────────────▶ (single /merge-to-main - └─ feat/author-owned-posts ──M1───M2───M3─── … ──▶ after task 8's panel) - └ tdd/task-1 ──╯ │ │ - └ tdd/task-2 ─╯ │ - └ tdd/task-3 ─╯ -``` - -- Merges into the integration branch are **`--no-ff`** — one merge commit - per task keeps the task boundary in history (the audit trail the - stacked-branch alternative would have provided, without its rebase - ceremony — the tasks are strict sequential dependencies, so true - stacking buys nothing here). -- Task branches + worktrees are deleted after their merge (the "reset"). -- `main` is touched exactly once, at the very end, via `/merge-to-main` - after task 8's final review panel. `make ci` is the merge gate at BOTH - levels: before every integration-branch merge, and again at the final - merge to main. - -## Loop protocol (one iteration per task) - -1. **Analyze** (parent, persistent context): read `loop_state.md`, pick the - first non-done task, write a **self-contained brief** — scope, files, - PRD sections, relevant cross-iteration notes, exit criteria, and the - task's acceptance behavior (the outer BDD test /tdd will frame). -2. **`/tdd `** — the whole implementation happens inside the tdd - skill: parent as CONDUCTOR, outer acceptance test, inner red/green/ - refactor cycles, RED author and GREEN implementer as **separate - persistent Opus 5 agents** (the skill's default model is opus — NEVER - pass the `sonnet`/`haiku` modifiers). Worktree on a task branch cut - from the integration head. -3. **`/second-opinion`** on the completed task, scoped to the task diff — - tell it the base explicitly: `git diff feat/author-owned-posts...HEAD`. - pragma:security fires automatically on trust-boundary diffs; task 6 - (auth/credential surface) must include it — verify it fired, don't - assume. -4. **`/fix-pr`** with the synthesized review report pasted in. Re-run the - tdd suite after fixes land (fix-pr's own go vet/build check is not the - gate). -5. **Verify**: full `make ci` in the task worktree. Green or it doesn't - merge — red means fix now, or mark the task `blocked: ` in - loop_state and stop the loop for user input. -6. **Merge** the task branch into `feat/author-owned-posts` (`--no-ff`, - merge commit titled `task N: `), delete the task branch and - worktree. -7. **Update `loop_state.md`** (status, merge commit, notes — surprises and - decisions go to Cross-iteration notes) and schedule the next iteration. - -Statuses: `pending → in-tdd → review → done` (or `blocked: `). -Stop when every task is done or on any block. - -Standing rules (carried from the test-refactor loop): -- A skip is a failure; missing infra is a `t.Fatal` naming the target. -- Subscribe-before-write cursor pattern for every firehose wait; no sleeps. -- Test handles: suite canonical helpers only (`uniqueTestID`/`uniqueAccount`). -- Defects found in *existing* code that are out of task scope: /file-issue, - don't fix inline, note in loop_state. -- Briefs to any agent are self-contained (file:line, failure scenario, - expected behavior) — no agent reads this file's history to reconstruct - context. - -## Task partition (each row ≈ one PR-sized /tdd run) - -Sizing rationale: every task leaves `make ci` green. The contract-manifest -gate forces consumers and their e2e contracts to land together (task 5); the -read-path filter (task 7) lands *after* the write flip (task 6) so -status-agnostic reads keep serving both old and new posts during the middle -of the loop. - -### Phase A — foundations (no behavior change) - -**1. Lexicons + fixtures** -PRD §3. New `internal/atproto/lexicon/social/coves/community/postv2.json` -(no `author`, `community` immutable-on-update documented), -`community/acceptance.json` (key `any`, deterministic-rkey doc, strongRef -subject), `community/removal.json` (open `knownValues` code + maxLength 64). -Deprecation note in `community/post.json` description ONLY (schema -untouched — it's published). Fixture tree: `tests/lexicon-test-data/postv2/` -valid+invalid, `acceptance/`, `removal/`; extend the record-lexicon -validation T0 tests. Acceptance behavior for the outer test: the three new -lexicons validate their valid fixtures and reject each invalid fixture with -the expected error. Exit: T0 green; no consumer references yet. - -**2. Migration 034 + admissions repository** -PRD §6.1, §5.3. `034_author_owned_posts.sql`: `community_post_admissions` -(PK (community_did, post_uri), status, acceptance_uri/rkey, accepted_cid, -decision_code, decision_at, evaluated_cid, redrivable, last_community_rev, -partial index on accepted); drop `posts` FK to users + CASCADE (soft ref). -`internal/db/postgres` admissions repo: upserts, status transitions, -watermark compare-and-set. Acceptance behavior: the T1 transition matrix — -stale-rev events are no-ops, same-rev removal wins, every legal/illegal -status transition. Exit: T1 green. - -**3. `admitPost` extraction + real admission policy** -PRD §4.1, §8. Extract CreatePost's checks into a shared `admitPost` -(community exists, visibility, aggregator authz) and ADD the new policy: -ban lookup against indexed ban state, per-author/per-community submission -rate limits, dedupe by (author, community, content CID) — each with typed -error → decision code. Wire into the EXISTING write-forward path (bans -start being enforced now; that's intended policy, call it out in the merge -commit). Acceptance behavior: a banned author's create is refused with the -ban code end-to-end on the current path. Exit: T0 matrix covers every -branch; T1 for ban/rate lookups; e2e still green. - -### Phase B — the new machinery - -**4. Acceptance engine + community-repo writers** -PRD §3.2, §3.3, §5.5, §5.6. Deterministic rkey helper (subject AT-URI → -rkey-safe transform) with T0 round-trip tests; acceptance writer -(putRecord + swapRecord conflict path); atomic acceptance-delete + removal -via `com.atproto.repo.applyWrites`; engine core: consume a -pending/pending_reacceptance admission → admitPost → write/update -acceptance | record rejection (AppView-local, redrivable=false) | atomic -remove. Removal terminality + bridgedStats-only repin decision function -(pure, T0). Acceptance behavior: engine double-fired on the same subject -converges to exactly one acceptance record (idempotence), proven against -the real PDS container at T1. Exit: T0/T1 green; nothing triggers the -engine in prod paths yet. - -**5. Ingestion: postv2 + acceptance + removal consumers, contracts** -PRD §5 entire. postv2 handler in `PostEventConsumer` (author = event.Did, -community from record + immutability enforcement, unknown-author soft -handling + opportunistic SSRF-safe identity hydration, pending admission -row, edit → CID compare → pending_reacceptance, delete → tombstone); -acceptance/removal handlers (community-repo authority check, §5.2 watermark -gating, acceptance-before-post → CID-verified direct PDS fetch with -SSRF/size/time caps, DLQ backstop); engine triggered for hosted -communities; BridgeTrust re-keyed to author repo. -`consumerWantedCollections` gains the three collections; THREE -`//coves:ingestion-contract` e2e contracts. Acceptance behavior (the outer -test IS the primary contract): postv2 written to an author repo on the -hermetic PDS → firehose → pending → auto-accept → accepted; plus banned -author never accepted; edit → re-accept; removal → atomic commit observed. -Old `community.post` consumer path stays live in parallel. This is the -hardest task — the parent may split it 5a (consumers) / 5b (contracts) at -brief-writing time, but WantedCollections + contracts must land in the -same merge. Exit: full `make ci` green. - -### Phase C — the flip - -**6. Write path flip** *(review must include pragma:security)* -PRD §4. CreatePost: session-explicit signature -(`*oauth.ClientSessionData`, comments' `PDSClientFactory` pattern), write -postv2 to author repo, blobs/thumbnails to author PDS (blob service gains -author BlobOwner path), aggregator stored-token path, sync fast-path -acceptance via engine, no rollback of author records on acceptance -failure. `post.delete` → author-session delete. `post.update` NEW endpoint -(route, handler, service, swapRecord conflict handling). Update e2e flows -that create posts. Acceptance behavior: a user's create lands in THEIR -repo, is accepted synchronously, and round-trips through the feed; an -acceptance-write failure leaves the author record intact and pending. -Exit: full `make ci`. - -**7. Read path: centralized visibility** -PRD §6.2, §3.4. One admission-aware predicate (view or shared query -helper); convert the inventory: community feeds ×4 + communityFeeds, -post.get (+ `#removedPost` union + admission context in postView), -getComments post hydration, actor surfaces, search, embed hydration, -counts/stats. `getStatus` query endpoint. Author self-view semantics. -Acceptance behavior: pending/removed posts are unreachable through EVERY -alternate endpoint (comments, search, counts) for non-authors, while the -author still sees status. Exit: full `make ci`. - -### Phase D — cutover - -**8. Drain, cleanup, cutover tooling** *(final panel reviews the whole branch)* -PRD §10. Re-materialization script (`cmd/` tool): per-post ledger, -resumable/idempotent, write postv2 to author repo → acceptance → verify -both + pinned CID → checkpoint → delete old community-repo record; tested -hermetically against the e2e stack with seeded old-style data. Remove: old -`community.post` from WantedCollections + its consumer branch + -community-credential post writes + `author`-field handling; old fixtures. -`federation-prd.md` superseded-by header. Docs pass over the PRD (mark -implemented, log divergences). Delete `plan.md` + `loop_state.md` before -the final merge (build-loop trackers, not suite artifacts — precedent: -`512e00e`). Acceptance behavior: the script, run twice against a seeded -hermetic stack, converges with a complete ledger and zero old-style -records. Exit: full `make ci`; task's /second-opinion runs on the WHOLE -branch diff vs main; then `/merge-to-main`. Prod execution of the script -is MANUAL, outside the loop, coordinated with coves-mobile/coves-frontend -URI-parsing updates (PRD §10.2). - -### Deferred (NOT loop tasks) - -- **Beta remote path** (PRD §7): `post.notify`, service-auth middleware - (`lxm`/`aud`/replay), DID service entry, local-vs-remote detection. - Explicitly deferred per owner decision 2026-08-07. -- Private-community admission leakage (PRD §11.4), snapshot dial (§11.1), - separate bridged-stats record (§11.3). -- 2.51.2