Repository navigation
feat(blog): blog v7 contract and client (elected moderation, action fees, timelines, post tombstones) - #692
Conversation
Elected per-contract moderation (seat contests, 30-day challenge cool-down, 7-day join / 3-day vote windows, owner protected, interim contract owner). The comment tokenCost is gone; blog, blogPost and blogComment creates pay feeMultiplier action fees (80M / 80M / 16M). New blog.timeline and blogPost.timeline [$createdAt] indexes, a ranked 72h discussedRecent window, and followersByDay replaced by a 72h followersTrend. The count twins merge into postAndTime and followers; blogPost.ownerAndTime, blogComment.ownerAndTime, blogFollow.following and the all-time comment ranking are dropped. Authors delete a post with a tombstone: deleted plus comments off, every content field blank (tombstoneIsBlank), hasBody for live posts, deleted frozen once set and retractedWhen so a barred author can still retract. publishedAt may not run 10 minutes past a now-required $updatedAt; the slug pattern matches lib/utils/slug.ts and image URLs are https or ipfs. The constraint table gains the ten tombstone and publishedNotAhead cases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Writes: blog, blogPost and blogComment creates carry the action fee agreement, read off the committed contract (lib/blog/blog-contract.ts) and attached through declaredActionFeeFor like social post/reply writes; the YAPP comment payment and its UI apply to v2-v6 only. Reads: discovery's Newest pages blog.timeline; a new Posts view lists the latest posts across blogs (blogPost.timeline) and the most discussed posts over the 72h discussedRecent window; Trending reads followersTrend (3 days); a reader's follows ride ownerAndBlog; comment and follower counts read the merged indexes with the same queries. Explore's blog tab reads the post timeline. The unused owner-order post and comment reads, whose indexes v7 drops, are gone. Delete: the author dashboard deletes a post by writing the tombstone (deleted, comments off, blogId/slug/publishedAt kept). Tombstones leave every listing, their link and embed say the post was deleted, and the comment path refuses them before signing. Bounds: blogPostDate mirrors publishedNotAhead (10 minutes past $updatedAt); empty image URLs are left out and v7 refuses non-https/ipfs ones with a message before signing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every create of a priced doctype now carries the declared action fee agreement (battery-lib attemptCreate takes an agreement and sends the hand-built batch). The YAPP pre-flight and accounting are gone; b3c is a comment with no agreement (40132) and b9 one agreeing to the wrong amount (40133). Counts and rankings read the merged postAndTime / followers indexes and the 72h discussedRecent / followersTrend windows. New cases: b22 an author's tombstone (the shared refused shapes, frozen deleted and publishedAt, the slug kept), b23 a banned author retracting its own post and nothing else (retractedWhen), b24 a comment on a tombstone. The self-test pins the v7 shape: elected moderation, no tokenCost, the fees, the timelines, the dropped indexes, retractedWhen. Self-test only; nothing was broadcast. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes: - A Load more still in flight when the sort changes no longer appends to the new list, and a failed sort switch no longer shows the previous list (useCursorList, shared by the blog and post discovery lists). - Comment notifications skip posts their author has deleted. - The owner's per-blog post count leaves deleted posts out. - The 10-minute publishedAt allowance applies only to posts that store $updatedAt (v7), so v1-v6 dating is unchanged. - A publish refused for a device clock more than 10 minutes ahead (publishedNotAhead) says so. - The tombstone writer's drift log no longer names a social helper for a blog post. Simplifications: PillTabs; enrichBlogPostsWithBlogNames; getBlogUrl; imageUrlProblem; a POST_STATUS table on the dashboard; one newest-first cursor page in BaseDocumentService and one getAllBlogs loop; the blog fee and trend grids parsed by contract-topology's actionFeeOf and timeRangeOf; blog-stats reuses windowClause; trendingBlogsCopy. Tests cover the real tombstone payload (custom base, a draft, an existing tombstone) and the notification filter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
createBattery takes an agreementFor(sdk, docType) hook that every create on the battery's contract uses unless the case names its own agreement or opts out; verify-blog passes seed-lib's feeAgreementFor with the blog schemas (actionFeeFor/feeAgreementFor take a schemas parameter) instead of wrapping the battery. The 40132/40133 matchers live once in social-battery-lib. verify-blog gains ensureFixtureBlog, a fixture-owned replace and a module-level index helper, and reuses the constraint table's drop. The "publishedAt ahead" constraint case moves to a day ahead so a long live run still finds it ahead when it reaches b19. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🔍 Review in progress — actively reviewing now (commit 6e199a0) · triage: critical · Phase 2 only (no Phase 1 for this repository) |
Deploying yappr with
|
| Latest commit: |
6e199a0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6a1e77d2.yappr.pages.dev |
| Branch Preview URL: | https://feat-blog-v7.yappr.pages.dev |
Deploying yappr-v2 with
|
| Latest commit: |
6e199a0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://06916b16.yappr-v2.pages.dev |
| Branch Preview URL: | https://feat-blog-v7.yappr-v2.pages.dev |
Brings in #689 (getNewestBlogs with its capped, cached scan) and #690 (social v13 and per-network interim registration). - blogService.getNewestBlogs gains the v7 branch: the head of blog.timeline, always complete. v1-v6 keep the capped scan. The old getAllBlogs is gone; explore, search and discovery all go through getNewestBlogs (v7 discovery still pages getBlogTimelinePage). - Discovery's "newest among the first N blogs" notice survives on the shared cursor-list hook, as its `incomplete` flag. - BaseDocumentService keeps both queryAll (#689) and the newest-first cursor page. - Blog v7's file interim stays contractOwner, which #690's withInterim registers as notYetUsable on mainnet; the docs say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 2 only (no Phase 1 for this repository)
Verified the supplied findings against exact head 625daee and consolidated the duplicate reviewer reports into three in-scope blockers: two discovery regressions and a battery timeout-reconciliation failure. This was a static review; the supplied CI snapshot reports successful lint, type checks, unit/component tests, builds, and dead-code checks, while standard E2E was skipped and non-blocking devnet E2E remained in progress.
🔴 3 blocking
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: general); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This large, intricate diff changes consensus-enforced moderation, document constraints and fee allocation in contracts/yappr-blog-contract.json, and changes funds-payment agreements and pre-signing fee guards in lib/services/state-transition-service.ts. - Phase 1 reviewers: not run (disabled for this repository)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer - Model comparison: every Phase-2 reviewer also ran on
gpt-6-astra; the verifier weighed both sets without knowing which model wrote which
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `app/explore/page.tsx`:
- [BLOCKING] app/explore/page.tsx:117-120: Continue past filtered timeline pages in Explore
`getLatestPosts({ limit: 20 })` limits the raw timeline documents before filtering drafts and tombstones, and returns `nextCursor` when more documents may remain. This branch discards that cursor and finishes loading immediately. If the newest 20 documents are drafts or tombstones, Explore displays “No blog posts yet” even when published articles exist on the next page, and provides no continuation control. The previous `getRecentPosts` path refilled past drafts; the new Posts discovery also reads beyond empty filtered pages and preserves its continuation. Follow the cursor to obtain public posts, or expose continuation and distinguish a filtered page from an exhausted timeline.
In `lib/blog/content-utils.ts`:
- [BLOCKING] lib/blog/content-utils.ts:343-344: Exclude posts whose parent blog has been removed
The contract permits moderators to remove a `blog`, while its child posts remain valid through their `moderatedDocument` reference to the removal record. Those published posts can still appear in the new cross-blog timeline or discussed ranking, but `blogService.getMany` omits the removed parent. This helper nevertheless returns every original post, so Explore and Posts discovery render article cards whose links fail with “Blog not found”: `app/blog/page.tsx` requires the parent blog before fetching the post. The previous Explore path started from existing blogs and therefore excluded these orphaned articles. Filter against the resolved parent blogs before returning the hydrated posts, or implement an intentional readable route for posts with removed parents.
In `scripts/battery-lib.mjs`:
- [BLOCKING] scripts/battery-lib.mjs:317-321: Retain the derived document ID when an agreed create times out
The new agreement branch calls `createWithAgreement`, which derives the exact document ID before broadcasting but returns it only after `broadcastAndWait` succeeds. If the transition lands and confirmation throws a 504, `attemptWrite` receives no result, leaving `storedById` dependent on `findRecentByValues`. Both query variants in that fallback require `$ownerId`, but v7 removes the owner indexes from `blogPost` and `blogComment`, so the fallback cannot find either document type. Successfully committed fixture writes are consequently reported as failures; a timed-out b1 post can abort the battery with “fixture posts unavailable.” Expose and retain the derived ID before waiting so reconciliation can fetch by ID even when confirmation throws, or reconcile through the surviving document-type-specific indexes.
Explore's v7 blog tab read one timeline page and stopped, so a newest page of drafts and tombstones showed "No blog posts yet" while published posts sat on the next page. blogPostService.getLatestPublishedPosts reads on (up to maxPages) until it has the posts it wants, and the Posts discovery list uses it too. enrichBlogPostsWithBlogNames now drops a post whose blog did not come back: a moderator can remove a blog while its posts stay, and the post page needs the blog, so the card only led to "Blog not found". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
createWithAgreement derives the document id before broadcasting but only returned it after broadcastAndWait. A create that landed while its wait threw (the DAPI 504 quirk) left the battery reconciling by value through an owner index, which blog v7's blogPost and blogComment no longer have, so a committed fixture write read as a failure. createWithAgreement now hands the id to onDerivedId before signing, and attemptCreate keeps it for the by-id read back. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 2 only (no Phase 1 for this repository)
Verified the supplied Phase-2 findings against head 6dea5f5: timeout ID preservation and removed-parent filtering are fixed, but Explore still presents a capped, filtered timeline as empty without continuation. Current CI passes lint, type checking, unit tests, browser-component tests, build, dead-code checking, App, and Engine checks; devnet E2E and Cloudflare deployment remain pending, and standard E2E is skipped. Verification was static; no local builds or tests were run.
🔴 1 blocking
1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This large, cross-cutting diff changes funds movement through moderator action fees in contracts/yappr-blog-contract.json and the corresponding fee agreements and guards in lib/services/state-transition-service.ts, alongside intricate moderation and tombstone behavior. - Phase 1 reviewers: not run (disabled for this repository)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `app/explore/page.tsx`:
- [BLOCKING] app/explore/page.tsx:119-121: Continue past filtered timeline pages in Explore
(existing thread: https://github.com/PastaPastaPasta/yappr/pull/692#discussion_r4202933678)
The bounded reader fixes an empty first page, but Explore still discards `nextCursor` when the scan reaches its cap. With 100 newest documents consisting of drafts or tombstones and a published article immediately behind them, this call reads five 20-document pages and returns no posts with a valid continuation cursor. Explore then finishes loading and renders “No blog posts yet” / “Be the first to publish an article!” without a way to continue. The page-cap regression spec confirms that the reader preserves the cursor at this boundary; Posts discovery already retains it, distinguishes a partial-empty result, and offers Load more. Keep the bounded scan, but preserve its continuation and distinguish a filtered result from an exhausted timeline, with a continuation control or a link to paged Posts discovery.
…rafts Explore's v7 blog tab reads at most five timeline pages. When those held only drafts, tombstones or posts of removed blogs while the timeline went on, it showed "No blog posts yet". boundedPostListState tells that "filtered, more exist" case apart from an exhausted timeline: Explore then links to the paged Posts discovery (/blog?view=posts) instead of the empty state, and adds the same "Browse all posts" link under a list that has more after it. BlogDiscovery takes an initialView for the link. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The shared fee-agreement plumbing (battery agreementFor hook, seed-lib actionFeeFor/feeAgreementFor schemas parameter, the 40132/40133 matchers, actionFeeOf) was byte-identical on both branches and now equals #692's. declaredActionFeeFor routes all three priced contracts (social, blog v7, storefront v6); the YAPP path pays only on blog v2-v6 and storefront v2-v5. contracts/README.md keeps both cut paragraphs and the v7 blog / v6 storefront file entries, and the docs note that storefront v6 is registered on sakura (EDr9McRV...). contracts/yappr-storefront-contract.json is unchanged (sha256 c6c9c678...). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One conflict, scripts/seed/seed-lib.mjs actionFeeFor: #692 gave it a `schemas` parameter for blog v7, and this branch had moved its body to scripts/social-shapes.mjs actionFeeOf. Both kept: actionFeeFor(docType, schemas = social) delegates to the one actionFeeOf, so blog, social and the v13 report fee are read the same way. The web report fee needed no change: #692's declaredActionFeeFor routes a social-contract action to declaredActionFee, which reads v13's report create fee, so the report write agrees to it through the shared helper. Two suites that re-import modules per case (composite feed page, failed notification sources) get a 20 s timeout: under the full parallel run their first case's cold import passed 5 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Blog v7: the mainnet-ready blog contract from the approved design, and the Yappr client for it. The client takes it on with
NEXT_PUBLIC_BLOG_TOPOLOGY=v7. Nothing is registered or broadcast, and/devnetstays on v6 (.env.devnetis unchanged).Contract (
contracts/yappr-blog-contract.json, copied verbatim from the design)maxAddedModerators: 10andownerProtected. The file's interim team is the contract owner. Registration picks the network's interim the way social v13 does (feat(contracts): social v13 (mainnet candidate) and the blocks contract #690,withInterim): devnet keeps the owner, and mainnet registersnotYetUsable.validate-contract-offline --network mainnetshows the latter.tokenCostis removed. Creates now payfeeMultiplieraction fees to the moderators: blog 80M, blogPost 80M, blogComment 16M.blog.timeline [$createdAt]andblogPost.timeline [$createdAt]blogComment.discussedRecent, a ranked window: 72h range, a new window every 24hblogFollow.followersTrend(72h / 24h / 7-day ttl), replacingfollowersByDaypostAndTime(rangeCountable) and follower counts intofollowers(rangeCountable, ranked atblogId).blogPost.ownerAndTime,blogComment.ownerAndTime,blogFollow.followingand the all-time comment ranking.deletedflag, andtombstoneIsBlank, which requires comments present-and-off and every content field absenthasBodyfor live postsdeletedfrozen once setretractedWhen: {present: "deleted"}publishedNotAhead:publishedAt≤$updatedAt+ 10 min, with$updatedAtnow requiredlib/utils/slug.tsscripts/property-constraint-cases.mjsgains the 10 tombstone andpublishedAtaccept/refuse cases.Client
blog,blogPostandblogCommentcreates carry the$actionFeeAgreement, the same way social post and reply writes do. The amounts come off the committed contract (lib/blog/blog-contract.ts) throughdeclaredActionFeeFor, whichstate-transition-serviceuses for both the agreement and the unpriced-action guard. The YAPP comment payment and its UI now apply to v2–v6 only./bloggets a Blogs / Posts switch.blog.timelinewith Load more; Most followed; Trending (3 days) onfollowersTrend.blogPost.timeline; Most discussed (3 days) ondiscussedRecent.getFollowedBlogsusesownerAndBlogon v7.incounts still serve a whole list.tombstoneDocument(newbaseoption):{deleted: true, commentsEnabled: false}plus the kept fields.isPublishedBlogPostnow excludes them. Comment notifications on a deleted post are skipped too, and the owner's per-blog count leaves them out.blogPostDatemirrorspublishedNotAhead(10 minutes past$updatedAt) on posts that store$updatedAt(v7); older cuts date posts exactly as before.''.useCursorList. A new sort restarts the list from page one, and a Load more still in flight when the sort changes is dropped instead of appended.'', on every cut. Readers treat both the same.Battery (
scripts/verify-blog.mjs), self-test onlycreateBatterytakes anagreementFor(sdk, docType)hook, and verify-blog passes seed-lib'sfeeAgreementForwith the blog schemas.attemptCreatesends the hand-built batch, and a case can name its own agreement or opt out.social-battery-lib(verify-v8 and verify-v10 import them).deletedandpublishedAtare frozen and that the slug stays takenselfTestV7pins the v7 shape: elected moderation, notokenCost, the fees, the timelines, the dropped indexes andretractedWhen.Merged with #689 and #690
staging is merged in (no rebase). #689's
getNewestBlogsgains the v7 branch: on v7 it returns the head ofblog.timelineand is alwayscomplete. v1–v6 keep #689's capped, cached scan. Explore, search and discovery all go through it, and v7 discovery still pagesgetBlogTimelinePage. Discovery's "newest among the first N blogs" notice is kept on v1–v6.Follow-ups (not in this PR)
lib/transition-agreements.tsnow pulls the blog contract JSON (~15 KB) into the write path's bundle so the fee amounts cannot drift. The social contract JSONs are already bundled the same way. A generated constants module would trim it.scripts/seed/non-social/blog.mjsstill seeds the v6 shape (YAPP comments,commentCount). It needs the agreement plumbing before it can seed a v7 contract./devnetover toNEXT_PUBLIC_BLOG_TOPOLOGY=v7.Test plan
node scripts/validate-contract-offline.mjs contracts/yappr-blog-contract.json --network mainnet --cost: parses under both validators; create size ~7,010 B signed; documentCreateCost (new index values) is blog 248.5M, blogPost 492.6M, blogComment 90.6M, blogFollow 72.6Mnode scripts/validate-contract-offline.mjs --constraints: every case passes, including the 10 new blog v7 casesnode scripts/verify-blog.mjs --self-test, plus the storefront, pollr and v10 battery self-testsnpm run lint: eslint with the worktree's own config (the nested-worktree config clash is a known gotcha)npm run test: 1980 tests after the staging merge; specs added for content-utils, blog-contract/fees, post/blog/follow/stats/comment services, the real tombstone payload and the notification filternpm run buildnpm run lint:deadnpm run build:testing && npm run test:e2e(smoke): 24 passed, 4 skipped🤖 Generated with Claude Code