A community based topic aggregation platform built on atproto

feat(posts): GREEN cycle 1 — the write path lands in the author's repo master

Task 6 GREEN. Implementation only; no test file is touched (RED's a9744e1 is the contract). All 10 T0 and all 26 T1 reds are green. The record moves (§3.1, §4.2) SubmissionRkey the deterministic postv2 key: sha256 over (community DID, fingerprint, bucket) newline-delimited, a timestamp placed INSIDE the submission's own dedupe bucket (bucket start + digest offset mod the window), clock ID from further digest bits, encoded by syntax.NewTID so the one encoder in the tree is the one ParseTID ships beside. A non-positive window collapses to the epoch rather than dividing by zero, matching dedupeBucket's answer to the same misconfiguration. AdmissionDecision.DedupeBucket reported by reserveSubmission so the rkey is derived from THE SAME bucket the ledger deduped against. A second clock read at the call site would work almost always and fail exactly at a bucket boundary — the one case the deterministic key exists for. CreatePost admission -> open the AUTHOR's repo -> community token -> build -> enhance -> create-only PutRecordWithCommit(swapRecord "") at the derived rkey -> meter -> seed the row and settle it. The author-repo open is ahead of the community's token because it is the credential the post cannot be written without; a community whose token will not refresh costs a link preview, not a post. Nothing after the record commits may fail the request. postV2From the write boundary, and the single place the author field is dropped and $type re-stamped. The community answers before we do (§4.2 steps 4 and 5) AcceptanceEngine.AcceptSubmission row-pending + CID-match, then accept. The decider is NOT consulted: CreatePost has already run that policy, and the production decider reads an index the post is not in yet, so a fast path that re-decided would refuse every post it was handed. settleSubmission seeds UpsertPending under the URI the consumer builds, reports accepted when the row already is (a retry of a settled post), maps ErrCommunityNotHosted to pending without alarm, and cannot fail the request. Editing and deleting UpdatePost authority-equals-session before anything is fetched, then read -> preserve community and createdAt from the STANDING record -> put guarded by the CID just read. A lost swap is ErrConcurrentModification. The submission ledger is untouched. DeletePost split by collection: postv2 goes to the author's own repo under local authorization, the deprecated community.post keeps the old community-credential path intact until task 8. Supporting pds.ErrNoCommit VERIFIED AGAINST A LIVE PDS: a create-only put of IDENTICAL bytes is a 200 no-op with no commit object, evaluated before the swap guard; the same put of DIFFERENT bytes is InvalidSwap. Both mean "it is already there", and createAuthorRecord answers both by reporting what stands. Without the sentinel the byte-identical retry read as a malformed body. NewAuthorRepoFactory the production seam: a person's session passes through, an aggregator's stored tokens are resumed, and nothing to resume is ErrNoAuthorCredentials rather than a 401 nobody can act on. post.update service, handler, error mapping and route against the lexicon file that already existed. postv2.json originalAuthor/federatedFrom/location declared as optional `unknown` — additive-optional evolution. Unconstrained deliberately: no bridge has fixed their shape, and inventing one would commit the first real bridge to a contract we made up. Three deviations, flagged rather than buried 1. The fingerprint and the embed pipeline still take the deprecated PostRecord, converted once at the write boundary. Retyping them is not a golden-value change but a COMPILE break in admit_matrix_test.go and embed_conversion_test.go, which are in-package — it would take the whole posts test binary with it and leave every red unverifiable. Cycle 2, with the tests. It also keeps the fingerprint byte-stable across the deploy, so live dedupe rows survive the flip. 2. The external-embed thumb guard moved into validateCreateRequest. It is pure and needs no network, and two tests demand opposite things of its position otherwise: the create-validation test wants the author-repo open to be the first failure after admission, the handler's embed test wants a 400 on a stack with no author repo at all. validateEmbed is untouched — its test pins that it ignores a junk thumb. 3. buildAcceptanceEngine now returns the engine, so the write path and the queue driver settle the same subjects through one instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


+876 -221
12 changed files