diff --git a/convex/lib/public.ts b/convex/lib/public.ts index 33ed405aa..b3b019a0e 100644 --- a/convex/lib/public.ts +++ b/convex/lib/public.ts @@ -41,6 +41,7 @@ export type HydratableSkill = Pick< | 'canonicalSkillId' | 'forkOf' | 'latestVersionId' + | 'latestVersionSummary' | 'tags' | 'badges' | 'stats' diff --git a/convex/lib/skillSearchDigest.test.ts b/convex/lib/skillSearchDigest.test.ts index ef2c366e6..0cd3f988c 100644 --- a/convex/lib/skillSearchDigest.test.ts +++ b/convex/lib/skillSearchDigest.test.ts @@ -102,7 +102,7 @@ describe('extractDigestFields', () => { expect(digest).not.toHaveProperty('moderationEvidence') expect(digest).not.toHaveProperty('quality') - expect(digest).not.toHaveProperty('latestVersionSummary') + expect(digest).toHaveProperty('latestVersionSummary') expect(digest).not.toHaveProperty('moderationNotes') expect(digest).not.toHaveProperty('moderationSummary') expect(digest).not.toHaveProperty('resourceId') diff --git a/convex/lib/skillSearchDigest.ts b/convex/lib/skillSearchDigest.ts index 0b6e1d906..958e775e9 100644 --- a/convex/lib/skillSearchDigest.ts +++ b/convex/lib/skillSearchDigest.ts @@ -19,6 +19,7 @@ const SHARED_KEYS = [ 'canonicalSkillId', 'forkOf', 'latestVersionId', + 'latestVersionSummary', 'tags', 'badges', 'stats', diff --git a/convex/schema.ts b/convex/schema.ts index 41f202249..d1e494387 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -448,6 +448,17 @@ const skillSearchDigest = defineTable({ canonicalSkillId: v.optional(v.id('skills')), forkOf: forkOfValidator, latestVersionId: v.optional(v.id('skillVersions')), + latestVersionSummary: v.optional( + v.object({ + version: v.string(), + createdAt: v.number(), + changelog: v.string(), + changelogSource: v.optional( + v.union(v.literal('auto'), v.literal('user')), + ), + clawdis: v.optional(v.any()), + }), + ), tags: v.record(v.string(), v.id('skillVersions')), badges: badgesValidator, stats: statsValidator, diff --git a/convex/skills.listPublicPageV2.test.ts b/convex/skills.listPublicPageV2.test.ts index 4bc8c533e..d7c88d197 100644 --- a/convex/skills.listPublicPageV2.test.ts +++ b/convex/skills.listPublicPageV2.test.ts @@ -287,6 +287,48 @@ describe('skills.listPublicPageV2', () => { ) }) + it('falls back to db.get(latestVersionId) when latestVersionSummary is absent', async () => { + const oldRow = makeSkill('skills:old', 'old', 'users:1', 'skillVersions:1') + // Simulate a pre-backfill digest row without latestVersionSummary + delete (oldRow as Record).latestVersionSummary + + const paginateMock = vi.fn().mockResolvedValue({ + page: [oldRow], + continueCursor: 'next-cursor', + isDone: false, + pageStatus: null, + splitCursor: null, + }) + const getMock = vi.fn(async (id: string) => { + if (id.startsWith('users:')) return makeUser(id) + if (id.startsWith('skillVersions:')) return makeVersion(id) + return null + }) + const ctx = { + db: { + query: vi.fn(() => ({ + withIndex: vi.fn(() => ({ + order: vi.fn(() => ({ paginate: paginateMock })), + })), + })), + get: getMock, + }, + } + + const result = await listPublicPageV2Handler(ctx, { + paginationOpts: { cursor: null, numItems: 25 }, + sort: 'downloads', + dir: 'desc', + highlightedOnly: false, + nonSuspiciousOnly: false, + }) + + expect(result.page).toHaveLength(1) + expect(result.page[0]?.skill.slug).toBe('old') + // Should have fetched the version doc via db.get + expect(getMock).toHaveBeenCalledWith('skillVersions:1') + }) + it('does not swallow non-cursor paginate errors', async () => { const paginateMock = vi.fn().mockRejectedValue(new Error('database unavailable')) const ctx = { @@ -333,6 +375,12 @@ function makeSkill( canonicalSkillId: undefined, forkOf: undefined, latestVersionId, + latestVersionSummary: { + version: '1.0.0', + createdAt: 1, + changelog: '', + changelogSource: 'user' as const, + }, tags: {}, badges: {}, stats: { diff --git a/convex/skills.ts b/convex/skills.ts index dfe656b0e..aa4bb6f63 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -958,11 +958,7 @@ async function buildPublicSkillEntries( const entries = await Promise.all( skills.map(async (skill) => { // Use denormalized summary when available to avoid reading the full ~6KB version doc. - // HydratableSkill (from digest rows) won't have latestVersionSummary. - const summary = - 'latestVersionSummary' in skill - ? (skill as Doc<'skills'>).latestVersionSummary - : undefined + const summary = skill.latestVersionSummary const hasSummary = includeVersion && summary const [latestVersionDoc, ownerInfo] = await Promise.all([ includeVersion && !hasSummary && skill.latestVersionId