feat: order featured catalog by recency (#3168)

This commit is contained in:
Patrick Erichsen
2026-07-17 17:46:50 -07:00
committed by GitHub
parent 43c079e434
commit da965d681c
13 changed files with 205 additions and 133 deletions
+19 -8
View File
@@ -135,7 +135,7 @@ function makeFeedSkillEntry(index: number) {
function makeCtx(
packages: unknown[],
records: Record<string, unknown>,
options: { packageHighlighted?: boolean } = {},
options: { packageHighlightedAt?: number } = {},
) {
return {
db: {
@@ -149,12 +149,12 @@ function makeCtx(
apply(query);
return {
unique: vi.fn(async () =>
options.packageHighlighted
options.packageHighlightedAt !== undefined
? {
packageId: "packages:1",
kind: "highlighted",
byUserId: "users:moderator",
at: 1,
at: options.packageHighlightedAt,
}
: null,
),
@@ -237,7 +237,7 @@ describe("catalog feed projection", () => {
{
"packageReleases:1": makeRelease(),
},
{ packageHighlighted: true },
{ packageHighlightedAt: 1_784_280_000_000 },
),
{ family: "code-plugin" },
);
@@ -247,6 +247,7 @@ describe("catalog feed projection", () => {
id: "@openclaw/demo",
state: "available",
featured: true,
featuredAt: 1_784_280_000_000,
install: {
candidates: [
expect.objectContaining({
@@ -343,10 +344,19 @@ describe("catalog feed projection", () => {
it("projects highlighted official skills as featured install candidates", async () => {
const result = (await listOfficialSkillEntriesHandler(
makeCtx([makeSkill({ badges: { highlighted: { byUserId: "users:moderator", at: 1 } } })], {
"publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" },
"skillVersions:1": makeSkillVersion(),
}),
makeCtx(
[
makeSkill({
badges: {
highlighted: { byUserId: "users:moderator", at: 1_784_280_000_000 },
},
}),
],
{
"publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" },
"skillVersions:1": makeSkillVersion(),
},
),
{ publisherId: "publishers:1", cursor: null },
)) as { entries: unknown[]; isDone: boolean };
@@ -355,6 +365,7 @@ describe("catalog feed projection", () => {
id: "@openclaw/demo",
state: "available",
featured: true,
featuredAt: 1_784_280_000_000,
}),
]);
});
+5
View File
@@ -65,6 +65,7 @@ const catalogFeedEntryFields = {
v.literal("deprecated"),
),
featured: v.optional(v.boolean()),
featuredAt: v.optional(v.number()),
publisher: v.object({
id: v.string(),
trust: v.union(v.literal("official"), v.literal("community")),
@@ -137,6 +138,7 @@ async function buildEntry(
version,
state: "available",
featured: Boolean(highlighted),
...(highlighted ? { featuredAt: highlighted.at } : {}),
publisher: {
id: publisherId,
trust: "official",
@@ -210,6 +212,7 @@ async function buildSkillEntry(
const title = skill.displayName.trim() || slug;
const description = skill.summary?.trim();
const icon = skill.icon?.trim();
const highlightedAt = skill.badges?.highlighted?.at;
const packageName = `@${publisherId}/${slug}`;
if (!publisherId || !slug || !title) return null;
@@ -243,6 +246,7 @@ async function buildSkillEntry(
version: commit,
state: "available",
featured: isSkillHighlighted(skill),
...(highlightedAt !== undefined ? { featuredAt: highlightedAt } : {}),
publisher: {
id: publisherId,
trust: "official",
@@ -290,6 +294,7 @@ async function buildSkillEntry(
version: versionName,
state: "available",
featured: isSkillHighlighted(skill),
...(highlightedAt !== undefined ? { featuredAt: highlightedAt } : {}),
publisher: {
id: publisherId,
trust: "official",
+29 -18
View File
@@ -9708,14 +9708,31 @@ describe("httpApiV1 handlers", () => {
expect(codePluginCursors).toEqual([null, "downloads-cursor"]);
});
it("plugins list defaults featured browse to downloads sort", async () => {
const readinessCalls: unknown[] = [];
it("plugins list preserves combined newest-featured order across plugin families", async () => {
const newestFeatured = makeCatalogItem("newest-featured", {
family: "bundle-plugin",
updatedAt: 20,
stats: { downloads: 1, installs: 1, stars: 0, versions: 1 },
});
const olderPopular = makeCatalogItem("older-popular", {
family: "code-plugin",
updatedAt: 10,
stats: { downloads: 100, installs: 100, stars: 0, versions: 1 },
});
const runQuery = vi.fn((_, args: Record<string, unknown>) => {
if (hasPluginRecommendedScoreReadinessArgs(args)) {
readinessCalls.push(args);
return false;
}
return { page: [], isDone: true, continueCursor: "" };
expect(args).toEqual(
expect.objectContaining({
families: ["code-plugin", "bundle-plugin"],
highlightedOnly: true,
paginationOpts: { cursor: null, numItems: 7 },
}),
);
expect(args).not.toHaveProperty("family");
return {
page: [newestFeatured, olderPopular],
isDone: true,
continueCursor: "",
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
@@ -9725,17 +9742,11 @@ describe("httpApiV1 handlers", () => {
);
expect(response.status).toBe(200);
expect(readinessCalls).toEqual([]);
for (const [, args] of runQuery.mock.calls) {
if (hasPluginRecommendedScoreReadinessArgs(args)) continue;
expect(args).toEqual(
expect.objectContaining({
highlightedOnly: true,
sort: "downloads",
paginationOpts: { cursor: null, numItems: 7 },
}),
);
}
await expect(response.json()).resolves.toMatchObject({
items: [{ name: "newest-featured" }, { name: "older-popular" }],
nextCursor: null,
});
expect(runQuery).toHaveBeenCalledTimes(1);
});
it("plugins list downloads sort forwards to both plugin families and merges by downloads", async () => {
+26
View File
@@ -1777,6 +1777,32 @@ async function listPackages(
);
}
if (!effectiveFamily && options?.pluginFamilies?.length && highlightedOnly) {
const result = await runQueryRef<{
page: CatalogListItem[];
isDone: boolean;
continueCursor: string | null;
}>(ctx, internalRefs.packages.listPageForViewerInternal, {
families: options.pluginFamilies,
channel: channelParam.value,
isOfficial: isOfficial.value,
highlightedOnly: true,
category,
topic,
excludedScanStatuses: excludedScanStatuses.value,
viewerUserId: viewerUserId ?? undefined,
paginationOpts: { cursor: rawCursor, numItems: limit },
});
return json(
{
items: result.page,
nextCursor: result.isDone ? null : result.continueCursor,
},
200,
rate.headers,
);
}
if (!effectiveFamily && options?.pluginFamilies?.length) {
const shouldMarkDefaultDownloadCursor =
!sortParam.value && pluginDefaultSort === RECOMMENDED_FALLBACK_SORT;
+49 -8
View File
@@ -157,6 +157,7 @@ const listPageForViewerInternalHandler = (
listPageForViewerInternal as unknown as WrappedHandler<
{
family?: "skill" | "code-plugin" | "bundle-plugin";
families?: Array<"skill" | "code-plugin" | "bundle-plugin">;
channel?: "official" | "community" | "private";
isOfficial?: boolean;
executesCode?: boolean;
@@ -3719,31 +3720,71 @@ describe("packages public queries", () => {
expect(result.page.map((entry) => entry.name)).toEqual(["secret-plugin", "public-plugin"]);
});
it("sorts highlighted package pages by the requested install order", async () => {
const lowerInstall = makeDigest("lower-install", {
it("keeps highlighted package pages in newest-featured order", async () => {
const newestFeatured = makeDigest("newest-featured", {
updatedAt: 20,
stats: { downloads: 100, installs: 5, stars: 0, versions: 1 },
});
const higherInstall = makeDigest("higher-install", {
const olderFeatured = makeDigest("older-featured", {
updatedAt: 10,
stats: { downloads: 1, installs: 50, stars: 0, versions: 1 },
});
const newerSkill = makeDigest("newer-skill", {
family: "skill",
updatedAt: 30,
});
const { ctx } = makeDigestCtx({
highlightedBadges: [
{ packageId: lowerInstall.packageId },
{ packageId: higherInstall.packageId },
{ packageId: newerSkill.packageId, at: 300 },
{ packageId: newestFeatured.packageId, at: 200 },
{ packageId: olderFeatured.packageId, at: 100 },
],
exactDigests: [lowerInstall, higherInstall],
exactDigests: [newerSkill, newestFeatured, olderFeatured],
});
const result = await listPageForViewerInternalHandler(ctx, {
family: "code-plugin",
families: ["code-plugin", "bundle-plugin"],
highlightedOnly: true,
sort: "installs",
paginationOpts: { cursor: null, numItems: 10 },
});
expect(result.page.map((entry) => entry.name)).toEqual(["higher-install", "lower-install"]);
expect(result.page.map((entry) => entry.name)).toEqual(["newest-featured", "older-featured"]);
});
it("keeps official packages first without re-ranking featured recency within each group", async () => {
const newestCommunity = makeDigest("newest-community", {
isOfficial: false,
updatedAt: 30,
});
const newestOfficial = makeDigest("newest-official", {
isOfficial: true,
updatedAt: 20,
});
const olderOfficial = makeDigest("older-official", {
isOfficial: true,
updatedAt: 10,
});
const { ctx } = makeDigestCtx({
highlightedBadges: [
{ packageId: newestCommunity.packageId, at: 300 },
{ packageId: newestOfficial.packageId, at: 200 },
{ packageId: olderOfficial.packageId, at: 100 },
],
exactDigests: [newestCommunity, newestOfficial, olderOfficial],
});
const result = await listPageForViewerInternalHandler(ctx, {
highlightedOnly: true,
officialFirst: true,
paginationOpts: { cursor: null, numItems: 10 },
});
expect(result.page.map((entry) => entry.name)).toEqual([
"newest-official",
"older-official",
"newest-community",
]);
});
it("does not let stale personal ownerUserId expose private package digests", async () => {
+19 -27
View File
@@ -108,7 +108,6 @@ import {
MAX_PUBLISH_TOTAL_BYTES,
} from "./lib/publishLimits";
import {
compareRecommendationStats,
computeRecommendationScore,
RECOMMENDATION_SCORE_VERSION,
} from "./lib/recommendationScore";
@@ -1327,6 +1326,7 @@ function digestMatchesSearchFilters(
digest: PackageDigestLike,
args: {
family?: PackageFamily;
families?: PackageFamily[];
channel?: PackageChannel;
isOfficial?: boolean;
category?: string;
@@ -1335,6 +1335,7 @@ function digestMatchesSearchFilters(
},
) {
if (args.family && digest.family !== args.family) return false;
if (args.families?.length && !args.families.includes(digest.family)) return false;
if (args.channel && digest.channel !== args.channel) return false;
if (typeof args.isOfficial === "boolean" && digest.isOfficial !== args.isOfficial) {
return false;
@@ -2545,6 +2546,7 @@ async function fetchHighlightedPackageDigests(
ctx: DbReaderCtx,
args: {
family?: PackageFamily;
families?: PackageFamily[];
channel?: PackageChannel;
isOfficial?: boolean;
category?: string;
@@ -2577,6 +2579,7 @@ async function fetchHighlightedPackagePage(
ctx: DbReaderCtx,
args: {
family?: PackageFamily;
families?: PackageFamily[];
channel?: PackageChannel;
isOfficial?: boolean;
category?: string;
@@ -2591,32 +2594,14 @@ async function fetchHighlightedPackagePage(
const items = await Promise.all(
digests.map(async (digest) => await toPublicPackageListItem(ctx, digest)),
);
return items
.sort((a, b) => {
if (args.officialFirst) {
const official = Number(b.isOfficial) - Number(a.isOfficial);
if (official !== 0) return official;
}
if (args.sort === "recommended") {
const recommendation = compareRecommendationStats(a.stats, b.stats);
if (recommendation !== 0) return recommendation;
}
if (args.sort === "installs") {
const installs = b.stats.installs - a.stats.installs;
if (installs !== 0) return installs;
}
if (args.sort === "downloads") {
const downloads = b.stats.downloads - a.stats.downloads;
if (downloads !== 0) return downloads;
}
return (
b.updatedAt - a.updatedAt ||
b.createdAt - a.createdAt ||
a.family.localeCompare(b.family) ||
a.name.localeCompare(b.name)
);
})
.slice(0, args.numItems);
// fetchHighlightedPackageDigests follows the badge timestamp index newest-first.
// Preserve that editorial order instead of re-ranking Featured by popularity.
if (!args.officialFirst) {
return items.slice(0, args.numItems);
}
const official = items.filter((item) => item.isOfficial);
const community = items.filter((item) => !item.isOfficial);
return [...official, ...community].slice(0, args.numItems);
}
async function getPackageByNormalizedName(ctx: DbReaderCtx, normalizedName: string) {
@@ -3651,6 +3636,9 @@ export const listPageForViewerInternal = internalQuery({
family: v.optional(
v.union(v.literal("skill"), v.literal("code-plugin"), v.literal("bundle-plugin")),
),
families: v.optional(
v.array(v.union(v.literal("skill"), v.literal("code-plugin"), v.literal("bundle-plugin"))),
),
channel: v.optional(
v.union(v.literal("official"), v.literal("community"), v.literal("private")),
),
@@ -3713,6 +3701,7 @@ async function listPackagePageImpl(
ctx: DbReaderCtx,
args: {
family?: PackageFamily;
families?: PackageFamily[];
channel?: PackageChannel;
isOfficial?: boolean;
highlightedOnly?: boolean;
@@ -3728,6 +3717,9 @@ async function listPackagePageImpl(
if (args.channel === "private" && !args.viewerUserId) {
return { page: [], isDone: true, continueCursor: "" };
}
if (args.families?.length && !args.highlightedOnly) {
throw new Error("families is only supported for highlighted package pages");
}
if (args.category && !isPluginCategorySlug(args.category)) {
return { page: [], isDone: true, continueCursor: "" };
}
+13 -13
View File
@@ -105,36 +105,36 @@ describe("skills.listPublicPageV4", () => {
});
});
it("sorts highlighted recommended results by weighted score, then updatedAt", async () => {
it("keeps highlighted results in newest-featured order", async () => {
const result = await listPublicPageV4Handler(
makeHighlightedCtx([
makeDigest({
id: "updated",
slug: "updated-skill",
id: "newest",
slug: "newest-featured",
stars: 2,
installsAllTime: 10,
downloads: 10,
updatedAt: 400,
}),
makeDigest({
id: "downloads",
slug: "downloads-skill",
id: "older-popular",
slug: "older-popular",
stars: 2,
installsAllTime: 10,
downloads: 50,
updatedAt: 100,
}),
makeDigest({
id: "installs",
slug: "installs-skill",
id: "older-installed",
slug: "older-installed",
stars: 2,
installsAllTime: 20,
downloads: 0,
updatedAt: 100,
}),
makeDigest({
id: "stars",
slug: "stars-skill",
id: "oldest-starred",
slug: "oldest-starred",
stars: 3,
installsAllTime: 0,
downloads: 0,
@@ -145,10 +145,10 @@ describe("skills.listPublicPageV4", () => {
);
expect(result.page.map((entry) => entry.skill.slug)).toEqual([
"downloads-skill",
"installs-skill",
"updated-skill",
"stars-skill",
"newest-featured",
"older-popular",
"older-installed",
"oldest-starred",
]);
});
+2 -58
View File
@@ -106,10 +106,7 @@ import {
normalizePublisherHandle,
requirePublisherRole,
} from "./lib/publishers";
import {
computeRecommendationScore,
RECOMMENDATION_SCORE_VERSION,
} from "./lib/recommendationScore";
import { RECOMMENDATION_SCORE_VERSION } from "./lib/recommendationScore";
import {
AUTO_HIDE_REPORT_THRESHOLD,
MAX_ACTIVE_REPORTS_PER_USER,
@@ -7154,57 +7151,6 @@ function readDigestRankStat(
return digest.statsInstallsAllTime ?? digest.stats.installsAllTime ?? 0;
}
function readDigestRecommendationScore(digest: Doc<"skillSearchDigest">): number {
return (
(digest.recommendedScoreVersion === RECOMMENDATION_SCORE_VERSION
? digest.recommendedScore
: undefined) ??
computeRecommendationScore(
{
downloads: readDigestRankStat(digest, "downloads"),
installs: readDigestRankStat(digest, "installsAllTime"),
stars: readDigestRankStat(digest, "stars"),
},
{
createdAt: digest.createdAt,
updatedAt: digest.updatedAt,
},
)
);
}
function compareSkillDigestsForPublicSort(
a: Doc<"skillSearchDigest">,
b: Doc<"skillSearchDigest">,
sort: PublicListSort,
dir: "asc" | "desc",
) {
const multiplier = dir === "asc" ? 1 : -1;
switch (sort) {
case "downloads":
return (readDigestRankStat(a, "downloads") - readDigestRankStat(b, "downloads")) * multiplier;
case "recommended":
return (
(readDigestRecommendationScore(a) - readDigestRecommendationScore(b)) * multiplier ||
(a.updatedAt - b.updatedAt) * multiplier
);
case "stars":
return (readDigestRankStat(a, "stars") - readDigestRankStat(b, "stars")) * multiplier;
case "installs":
return (
(readDigestRankStat(a, "installsAllTime") - readDigestRankStat(b, "installsAllTime")) *
multiplier
);
case "updated":
return (a.updatedAt - b.updatedAt) * multiplier;
case "name":
return a.displayName.localeCompare(b.displayName) * multiplier;
case "newest":
default:
return (a.createdAt - b.createdAt) * multiplier;
}
}
type OfficialFirstSkillCategoryPageOptions = {
sort: PublicListSort;
dir: "asc" | "desc";
@@ -7476,7 +7422,7 @@ async function listOfficialFirstSkillCategoryPage(
};
}
/** Fetch highlighted skills via the skillBadges index, then sort in JS. */
/** Fetch highlighted skills newest-first via the skillBadges timestamp index. */
async function fetchHighlightedPage(
ctx: QueryCtx,
opts: {
@@ -7519,8 +7465,6 @@ async function fetchHighlightedPage(
digests.push(digest);
}
digests.sort((a, b) => compareSkillDigestsForPublicSort(a, b, opts.sort, opts.dir));
const trimmed = digests.slice(0, opts.numItems);
const items: PublicSkillEntry[] = [];
+6
View File
@@ -31,6 +31,7 @@ export declare const CatalogFeedPluginEntrySchema: import("arktype/internal/vari
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
@@ -60,6 +61,7 @@ export declare const CatalogFeedSkillEntrySchema: import("arktype/internal/varia
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
@@ -89,6 +91,7 @@ export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/o
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
@@ -116,6 +119,7 @@ export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/o
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
@@ -152,6 +156,7 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
@@ -179,6 +184,7 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object
version: string;
state: "available" | "blocked" | "deprecated" | "disabled" | "recommended";
featured?: boolean | undefined;
featuredAt?: number | undefined;
publisher: {
id: string;
trust: "community" | "official";
+8
View File
@@ -26,6 +26,7 @@ const CatalogFeedEntryBaseSchema = {
state: CatalogFeedStateSchema,
// Additive v1 metadata: existing hosted-feed consumers ignore unknown entry fields.
featured: "boolean?",
featuredAt: "number?",
publisher: {
"+": "reject",
id: "string",
@@ -81,6 +82,12 @@ export function parseCatalogFeed(value) {
if (Date.parse(feed.expiresAt) <= Date.parse(feed.generatedAt)) {
throw new Error("Catalog feed expiresAt must be after generatedAt");
}
for (const entry of feed.entries) {
if (entry.featuredAt !== undefined &&
(entry.featured !== true || !Number.isSafeInteger(entry.featuredAt) || entry.featuredAt < 0)) {
throw new Error("Catalog feed featuredAt requires a featured entry and epoch milliseconds");
}
}
return feed;
}
export function serializeCatalogFeed(feed) {
@@ -96,6 +103,7 @@ export function serializeCatalogFeed(feed) {
version: entry.version,
state: entry.state,
...(entry.featured === undefined ? {} : { featured: entry.featured }),
...(entry.featuredAt === undefined ? {} : { featuredAt: entry.featuredAt }),
publisher: {
id: entry.publisher.id,
trust: entry.publisher.trust,
File diff suppressed because one or more lines are too long
+18
View File
@@ -151,6 +151,7 @@ describe("catalog feed schema", () => {
entries: makeFeed().entries.map((entry, index) => ({
...entry,
featured: index === 0,
...(index === 0 ? { featuredAt: 1_784_280_000_000 } : {}),
})),
});
@@ -158,8 +159,25 @@ describe("catalog feed schema", () => {
expect(parsed.schemaVersion).toBe(1);
expect(parsed.entries.find((entry) => entry.id === "zeta")?.featured).toBe(true);
expect(parsed.entries.find((entry) => entry.id === "zeta")?.featuredAt).toBe(1_784_280_000_000);
expect(parsed.entries.find((entry) => entry.id === "alpha")?.featured).toBe(false);
expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("featured");
expect(parseCatalogFeed(makeFeed()).entries[0]).not.toHaveProperty("featuredAt");
});
it("rejects featured timestamps on entries that are not featured", () => {
expect(() =>
parseCatalogFeed({
...makeFeed(),
entries: [
{
...makeFeed().entries[0],
featured: false,
featuredAt: 1_784_280_000_000,
},
],
}),
).toThrow("featuredAt");
});
it("round-trips optional listing metadata without changing schema version 1", () => {
+10
View File
@@ -37,6 +37,7 @@ const CatalogFeedEntryBaseSchema = {
state: CatalogFeedStateSchema,
// Additive v1 metadata: existing hosted-feed consumers ignore unknown entry fields.
featured: "boolean?",
featuredAt: "number?",
publisher: {
"+": "reject",
id: "string",
@@ -107,6 +108,14 @@ export function parseCatalogFeed(value: unknown): CatalogFeed {
if (Date.parse(feed.expiresAt) <= Date.parse(feed.generatedAt)) {
throw new Error("Catalog feed expiresAt must be after generatedAt");
}
for (const entry of feed.entries) {
if (
entry.featuredAt !== undefined &&
(entry.featured !== true || !Number.isSafeInteger(entry.featuredAt) || entry.featuredAt < 0)
) {
throw new Error("Catalog feed featuredAt requires a featured entry and epoch milliseconds");
}
}
return feed;
}
@@ -123,6 +132,7 @@ export function serializeCatalogFeed(feed: CatalogFeed): string {
version: entry.version,
state: entry.state,
...(entry.featured === undefined ? {} : { featured: entry.featured }),
...(entry.featuredAt === undefined ? {} : { featuredAt: entry.featuredAt }),
publisher: {
id: entry.publisher.id,
trust: entry.publisher.trust,