RFC: Period-based popularity for publishers, skills, and plugins
Area
Product / UX
Context
ClawHub browse surfaces currently lean on all-time popularity or recency, which means the same established publishers, skills, and plugins can dominate discovery indefinitely.
A proposed improvement is to add period tabs: Weekly, Monthly, and All time.
The immediate UI request is narrow:
/publishers: add the period tabs to the right of thePopular publishersheading./skills: add the same period tabs above the listing, beside the existing List/Grid controls, without creating a separatePopular skillssection./plugins: do the same beside List/Grid, also without adding a separate popular section.
A local code investigation found that the UI placement is straightforward, but the data contract is not yet ready for correct period-based popularity across all three surfaces.
Current implementation facts:
/publisherscallsapi.publishers.listPublicPagefromsrc/routes/publishers/index.tsxand highlightspublishers.slice(0, 3)asPopular publishers.- Publisher ranking is currently all-time:
comparePublisherListItemsranks by downloads, stars, published count, then name. - Publisher schema stores all-time denormalized totals:
publishedSkills,publishedPackages,totalDownloads,totalInstalls,totalStars. /skillsusesapi.skills.listPublicPageV4for all-time-ish browse sorts such as downloads, stars, installs, updated, newest, and name.- Skills already have a weekly trending foundation through
skillDailyStats,skillLeaderboards, andlistPublicTrendingPage, but that endpoint is not the paginated general browse contract needed for the/skillslisting tabs. /pluginsusesfetchPluginCatalog->/api/v1/plugins, and backendlistPluginsV1Handlermerges code-plugin and bundle-plugin results byupdatedAt.packageSearchDigestdoes not currently include stats fields, and package browse indexes are mostly updated/filter indexes.packageStatEventsrecords download/install events but is indexed only byprocessedAtfor processing; package processing increments all-time package stats and does not write daily package stats today.
The core decision: should ClawHub expose period popularity only where it is already correct, or should we first establish a shared temporal popularity foundation across skills, plugins, and publishers?
Goals
- Make discovery more current: users should be able to see what is popular this week or month, not only what won all-time.
- Keep
/publishers,/skills, and/pluginssemantically aligned:WeeklyandMonthlyshould mean the same kind of period popularity across surfaces. - Avoid expensive Convex scans, especially for monthly windows and publisher aggregation.
- Keep all-time browse using existing denormalized totals and indexes where appropriate.
- Preserve stable pagination when leaderboards rebuild while a user is paging.
- Avoid misleading publisher rankings that count skills but silently ignore plugins.
- Keep the first UI implementation scoped and testable once the backend contract is ready.
Non-goals
- This RFC does not require changing the visual design of cards or browse pages beyond period tabs.
- This RFC does not propose a separate
Popular skillsorPopular pluginssection. - This RFC does not define final scoring weights for every metric beyond recommending that the first version stay simple and explainable.
- This RFC does not require replacing existing all-time sorting indexes with leaderboard snapshots.
- This RFC does not decide analytics dashboards or private admin reporting.
Proposal
Treat this as an RFC before implementing the visible tabs.
Recommended direction:
- Do not ship
/publishersWeekly/Monthlytabs backed only byskillDailyStatsunless the UI explicitly labels the result as skill-only activity. Calling thatPopular publisherswould be misleading for publishers whose primary activity is plugins. - Keep
All timebacked by current denormalized totals and indexes. Do not snapshot all-time into temporal leaderboard tables unless there is a separate reason later. - Add daily package stats before exposing plugin or publisher period popularity:
packageDailyStats:{ packageId, day, downloads, installs, updatedAt }- indexes similar to
skillDailyStats, at minimumby_package_dayandby_day - update
processPackageStatEventsInternalto write daily rows in addition to all-time totals - add a cursor-based backfill path from existing
packageStatEventsif the event history is retained and usable
- Add publisher-level daily aggregation so publisher period ranking does not have to scan all owned skills/packages across every requested window:
publisherDailyStats:{ publisherId, day, skillDownloads, packageDownloads, downloads, installs, updatedAt }- use ownership at event/day attribution time, not current ownership at query time, or explicitly choose and document a different rule
- Use separate leaderboard tables per entity family, but share TypeScript helpers/row shape where useful:
- extend or version
skillLeaderboardsfor weekly/monthly paginated browse - add
packageLeaderboardsor plugin-specific leaderboard storage - add
publisherLeaderboards
- extend or version
- Use snapshot-pinned pagination for temporal leaderboards:
- first page resolves a
snapshotIdorsnapshotAt - subsequent cursors include the snapshot identity and rank/cursor position
- old snapshots can be garbage-collected after a short retention window
- first page resolves a
- Define period semantics simply for V1:
Weekly= trailing 7 completed daysMonthly= trailing 30 completed days- avoid calendar-week/calendar-month semantics for the first implementation unless there is a strong product reason
- UI behavior:
/publishers: tabs sit besidePopular publishers; top highlight cards and list are derived from the same selected period result./skillsand/plugins: tabs sit beside List/Grid controls above the existing listing.- switching period resets pagination/cursor.
- empty or low-activity windows should show a clear empty state rather than silently falling back to all-time.
Suggested implementation sequence:
- Foundation PR: add
packageDailyStats, update package stat processing, and add package daily backfill. - Foundation PR: add
publisherDailyStatsor an equivalent bounded daily rollup derived from skill/package daily stats. - Leaderboard PR: add weekly/monthly leaderboard generation with snapshot-pinned pagination for skills, plugins, and publishers.
- UI PR for
/publishers: add tabs besidePopular publishers, backed by publisher period leaderboards for weekly/monthly and current all-time path for all-time. - UI PR for
/skillsand/plugins: add tabs beside List/Grid using the same period contract.
Examples
Good behavior:
- A new publisher whose plugin is heavily downloaded this week can appear in
Weeklypublisher results even if they have low all-time totals. - A skill with strong activity over the last 7 completed days appears in
Weeklyskills, whileAll timeremains ordered by existing downloads/stars/installs sort behavior. - A user switches from
WeeklytoMonthly; the page resets to the first page and does not reuse an old cursor. - A leaderboard rebuild happens between page 1 and page 2; the user's cursor remains pinned to the original snapshot, so there are no duplicates or skipped items.
Bad behavior to avoid:
/publishersWeeklycounts only skills while presenting the result as overall publisher popularity.Weeklysilently falls back to all-time when there is not enough activity.- Monthly leaderboards query raw stat event tables directly on every browse request.
- A single mixed generic leaderboard table grows to include the union of all fields needed by skills, plugins, and publishers, increasing read size and coupling unrelated rebuilds.
Edge cases:
- Ownership transfer during a week: the RFC should decide whether daily stats remain attributed to the owner at event time or move retroactively with current ownership. Event-time attribution is likely less surprising for a time-window leaderboard.
- Low activity: decide whether to show low-count leaders, require a minimum score threshold, or show an empty state.
- Plugins with both code-plugin and bundle-plugin families: define whether plugin popularity ranks package entities directly and ensure there is no double-counting through mixed catalog behavior.
User impact
Users get fresher discovery surfaces and can find currently active publishers, skills, and plugins instead of only all-time winners.
Publishers get a fairer path to visibility if recent work is popular, but only if the metric counts both skills and plugins honestly.
Maintainers get a clearer implementation boundary: UI tabs should not ship until the backing data contract is correct enough and tested.
API/CLI consumers are not directly affected unless public API sort/period parameters are later exposed; if they are, those parameters should follow the same period semantics.
Open questions
- Should
Weekly/Monthlyrank by downloads only, installs only, or a simple weighted score? For V1, downloads may be the most explainable baseline, but publisher all-time ranking currently also considers stars and published count as tie-breakers. - Should publisher period popularity include stars, or only activity events such as downloads/installs?
- Should low-activity weekly/monthly windows have a minimum threshold before showing results?
- Should ownership attribution be event-time, day-rollup-time, or current-owner-at-query-time?
- Do we want separate
pluginLeaderboardsfor code/bundle plugin families, or one package/plugin leaderboard filtered by family? - Is backfilling
packageDailyStatsfrom existingpackageStatEventsworth doing before launch, or is a forward-only warm-up acceptable? - Should
/skillsreplace the existing ad hoclistPublicTrendingPagebehavior with the new paginated period contract once available?
Validation plan
Backend tests:
- Daily package stat processing writes both all-time totals and
packageDailyStatsrows. - Package daily backfill is idempotent and cursor-based.
- Publisher daily aggregation includes both skill and package activity.
- Weekly and monthly leaderboards read daily stats day-by-day, not through unbounded full scans.
- Snapshot-pinned pagination does not duplicate or skip items after a rebuild.
- All-time paths continue using existing denormalized totals/indexes.
- Ownership-transfer behavior is explicitly tested according to the chosen attribution rule.
Frontend tests:
/publishersperiod tab changes the query/search state and resets pagination./publishershighlight cards derive from the selected period result./skillsand/pluginsperiod tabs live beside List/Grid and reset cursor/page state.- Empty/low-activity weekly/monthly states do not silently display all-time results.
Manual checks:
- Verify
/publishers,/skills, and/pluginson desktop and mobile. - Verify list/grid controls still work with each period.
- Verify query/filter interactions preserve the selected period only where semantically valid.
Source: openclaw/clawhub