diff --git a/AGENTS.md b/AGENTS.md index 17c753f619..1db93dc59f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,3 +47,12 @@ - New Convex functions must be pushed before `convex run`: use `bunx convex dev --once` (dev) or `bunx convex deploy` (prod). - For non-interactive prod deploys, use `bunx convex deploy -y` to skip confirmation. - If `bunx convex run --env-file .env.local ...` returns `401 MissingAccessToken` despite `bunx convex login`, workaround: omit `--env-file` and use `--deployment-name ` / `--prod`. + +## Convex Query & Bandwidth Rules +- **Always use `.withIndex()` instead of `.filter()` for fields that can be indexed.** `.filter()` causes full table scans — every doc is read and billed. Even a single `.filter()` on a 16K-row table reads ~16 MB per call. +- **Convex reads entire documents** — no field projections. If you only need a few fields from large docs (~6 KB+), denormalize a lightweight summary onto the parent doc or use a lookup table (see `embeddingSkillMap`, `skill.latestVersionSummary`, `skill.badges` for examples). +- **Denormalization pattern**: persist computed fields so they can be indexed. Every mutation that updates source fields must also update the denormalized field. Always write a cursor-based backfill for new fields (see `backfillIsSuspiciousInternal`, `backfillLatestVersionSummaryInternal`, `backfillDenormalizedBadgesInternal` for examples). +- **Cron jobs must never scan entire tables.** Use indexed queries with equality filters. Use cursor-based pagination for large datasets. Prefer incremental/delta tracking over full recounts. +- **32K document limit per query.** Split `.collect()` calls by a partition field (e.g., one day at a time instead of a 7-day range). See `buildTrendingLeaderboard` for an example. +- **Common mistakes**: `.filter().collect()` without an index; `ctx.db.get()` on large docs in a loop for list views; while loops that paginate the whole table to find filtered results. +- **Before writing or reviewing Convex queries, check deployment health.** Run `bunx convex insights` to check for OCC conflicts, `bytesReadLimit`, and `documentsReadLimit` errors. Run `bunx convex logs --failure` to see individual error messages and stack traces. This helps identify which functions are causing bandwidth issues so you can prioritize fixes. diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb7e67960..b01452b42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Admin API: `POST /api/v1/users/reclaim` now performs non-destructive root-slug owner transfer (preserves existing skill versions/stats/metadata) and clears active slug reservations. - VirusTotal: use shared AV-engine fallback verdict mapping for pending/backfill flows and keep undetected-only results pending (#591) (thanks @Shuai-DaiDai). +- Skills/listing: keep non-suspicious browse pagination on one cursor family during `isSuspicious` backfill, and re-sync stale `latestVersionSummary` metadata fields (#572) (thanks @sethconvex). - CLI publish: use a longer multipart upload timeout and normalize abort rejections into proper Errors (#550) (thanks @MunemHashmi). - CLI: forward optional auth tokens for `search` and `explore` against authenticated registries (#608) (thanks @artdaal). - Skill metadata: parse top-level `requires.*`, `primaryEnv`, and homepage fallbacks for security review accuracy (#548) (thanks @MunemHashmi). diff --git a/convex/comments.query.test.ts b/convex/comments.query.test.ts index f6e9cf1083..36dae0e96f 100644 --- a/convex/comments.query.test.ts +++ b/convex/comments.query.test.ts @@ -1,6 +1,6 @@ /* @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { listBySkill } from './comments' +import { listBySkillHandler } from './comments' function makeCtx(args: { comments: Array> @@ -45,7 +45,7 @@ describe('comments.listBySkill', () => { }, }) - const result = await listBySkill._handler(ctx, { + const result = await listBySkillHandler(ctx, { skillId: 'skills:1', limit: 50, } as never) @@ -115,7 +115,7 @@ describe('comments.listBySkill', () => { }, }) - const result = await listBySkill._handler(ctx, { + const result = await listBySkillHandler(ctx, { skillId: 'skills:1', limit: 50, } as never) diff --git a/convex/comments.ts b/convex/comments.ts index ed6e72fb2b..3ddab277f1 100644 --- a/convex/comments.ts +++ b/convex/comments.ts @@ -6,26 +6,28 @@ import { type PublicUser, toPublicUser } from './lib/public' export const listBySkill = query({ args: { skillId: v.id('skills'), limit: v.optional(v.number()) }, - handler: async (ctx, args) => { - const limit = args.limit ?? 50 - const comments = await ctx.db - .query('comments') - .withIndex('by_skill', (q) => q.eq('skillId', args.skillId)) - .order('desc') - .take(limit) - - const rows = await Promise.all( - comments.map(async (comment): Promise<{ comment: Doc<'comments'>; user: PublicUser } | null> => { - if (comment.softDeletedAt) return null - const user = toPublicUser(await ctx.db.get(comment.userId)) - if (!user) return null - return { comment, user } - }), - ) - return rows.filter((row): row is { comment: Doc<'comments'>; user: PublicUser } => row !== null) - }, + handler: listBySkillHandler, }) +export async function listBySkillHandler(ctx: import('./_generated/server').QueryCtx, args: { skillId: import('./_generated/dataModel').Id<'skills'>; limit?: number }) { + const limit = args.limit ?? 50 + const comments = await ctx.db + .query('comments') + .withIndex('by_skill', (q) => q.eq('skillId', args.skillId)) + .order('desc') + .take(limit) + + const rows = await Promise.all( + comments.map(async (comment): Promise<{ comment: Doc<'comments'>; user: PublicUser } | null> => { + if (comment.softDeletedAt) return null + const user = toPublicUser(await ctx.db.get(comment.userId)) + if (!user) return null + return { comment, user } + }), + ) + return rows.filter((row): row is { comment: Doc<'comments'>; user: PublicUser } => row !== null) +} + export const add = mutation({ args: { skillId: v.id('skills'), body: v.string() }, handler: addHandler, diff --git a/convex/crons.ts b/convex/crons.ts index 083d1c64f1..fb1892a93b 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -45,7 +45,7 @@ crons.interval( crons.interval( 'global-stats-update', - { minutes: 60 }, + { hours: 24 }, internal.statsMaintenance.updateGlobalStatsInternal, {}, ) diff --git a/convex/lib/leaderboards.ts b/convex/lib/leaderboards.ts index 02d33e66c8..8843f771f6 100644 --- a/convex/lib/leaderboards.ts +++ b/convex/lib/leaderboards.ts @@ -27,17 +27,27 @@ export async function buildTrendingLeaderboard( ) { const now = params.now ?? Date.now() const { startDay, endDay } = getTrendingRange(now) - const rows = await ctx.db - .query('skillDailyStats') - .withIndex('by_day', (q) => q.gte('day', startDay).lte('day', endDay)) - .collect() + // Query one day at a time to stay well under the 32K document limit. + // Each daily query reads ~4,500 docs instead of 32K for the full 7-day range. + // Parallelized since there are no cross-day dependencies. + const dayKeys = Array.from({ length: endDay - startDay + 1 }, (_, i) => startDay + i) + const perDayRows = await Promise.all( + dayKeys.map((day) => + ctx.db + .query('skillDailyStats') + .withIndex('by_day', (q) => q.eq('day', day)) + .collect(), + ), + ) const totals = new Map, { installs: number; downloads: number }>() - for (const row of rows) { - const current = totals.get(row.skillId) ?? { installs: 0, downloads: 0 } - current.installs += row.installs - current.downloads += row.downloads - totals.set(row.skillId, current) + for (const rows of perDayRows) { + for (const row of rows) { + const current = totals.get(row.skillId) ?? { installs: 0, downloads: 0 } + current.installs += row.installs + current.downloads += row.downloads + totals.set(row.skillId, current) + } } const entries = Array.from(totals, ([skillId, totalsEntry]) => ({ diff --git a/convex/lib/skillSafety.ts b/convex/lib/skillSafety.ts index a0d56e8f26..a8183ee449 100644 --- a/convex/lib/skillSafety.ts +++ b/convex/lib/skillSafety.ts @@ -11,3 +11,13 @@ export function isSkillSuspicious( if (skill.moderationFlags?.includes('flagged.suspicious')) return true return isScannerSuspiciousReason(skill.moderationReason) } + +/** + * Compute the denormalized `isSuspicious` boolean for a skill. + * Use at every mutation site that writes `moderationFlags` or `moderationReason`. + */ +export function computeIsSuspicious( + skill: Pick, 'moderationFlags' | 'moderationReason'>, +): boolean { + return isSkillSuspicious(skill) +} diff --git a/convex/maintenance.test.ts b/convex/maintenance.test.ts index 2493485f30..5de81a4ec4 100644 --- a/convex/maintenance.test.ts +++ b/convex/maintenance.test.ts @@ -33,6 +33,7 @@ vi.mock('./lib/skillSummary', () => ({ })) const { + backfillLatestVersionSummaryInternal, backfillSkillFingerprintsInternalHandler, backfillSkillSummariesInternalHandler, cleanupEmptySkillsInternalHandler, @@ -195,6 +196,66 @@ describe('maintenance backfill', () => { }, }) }) + + it('re-syncs latestVersionSummary when changelogSource or clawdis drift', async () => { + const paginate = vi.fn().mockResolvedValue({ + page: [ + { + _id: 'skills:1', + latestVersionId: 'skillVersions:1', + latestVersionSummary: { + version: '1.0.0', + createdAt: 123, + changelog: 'Same changelog', + changelogSource: 'user', + clawdis: undefined, + }, + }, + ], + continueCursor: null, + isDone: true, + }) + const get = vi.fn().mockResolvedValue({ + _id: 'skillVersions:1', + version: '1.0.0', + createdAt: 123, + changelog: 'Same changelog', + changelogSource: 'auto', + parsed: { clawdis: { emoji: 'lobster' } }, + }) + const patch = vi.fn().mockResolvedValue(undefined) + const runAfter = vi.fn() + + const ctx = { + db: { + query: vi.fn(() => ({ paginate })), + get, + patch, + }, + scheduler: { + runAfter, + }, + } as never + + const result = await ( + backfillLatestVersionSummaryInternal as unknown as { _handler: Function } + )._handler(ctx, { + batchSize: 10, + }) + + expect(result).toEqual({ patched: 1, isDone: true, scanned: 1 }) + expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 10 }) + expect(patch).toHaveBeenCalledWith('skills:1', { + latestVersionSummary: { + version: '1.0.0', + createdAt: 123, + changelog: 'Same changelog', + changelogSource: 'auto', + clawdis: { emoji: 'lobster' }, + }, + }) + expect(runAfter).not.toHaveBeenCalled() + }) }) describe('maintenance badge denormalization', () => { diff --git a/convex/maintenance.ts b/convex/maintenance.ts index fb9c911580..e85530a6d7 100644 --- a/convex/maintenance.ts +++ b/convex/maintenance.ts @@ -12,6 +12,7 @@ import { type TrustTier, } from './lib/skillQuality' import { generateSkillSummary } from './lib/skillSummary' +import { computeIsSuspicious } from './lib/skillSafety' import { hashSkillFiles } from './lib/skills' const DEFAULT_BATCH_SIZE = 50 @@ -1529,6 +1530,107 @@ export const backfillDenormalizedBadgesInternal = internalMutation({ }, }) +/** + * Backfill `latestVersionSummary` on all skills. Cursor-based paginated mutation + * that self-schedules until done. Reads each skill's latestVersionId, extracts + * the summary fields, and patches the skill. + * + * Always reconciles against the current `latestVersionId` — if the summary is + * stale (e.g. from a tag retarget), it will be rewritten. To force a full + * re-backfill, simply re-run the function; every row is re-evaluated. + */ +export const backfillLatestVersionSummaryInternal = internalMutation({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const batchSize = clampInt(args.batchSize ?? 50, 10, 200) + const { page, continueCursor, isDone } = await ctx.db + .query('skills') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + + let patched = 0 + for (const skill of page) { + if (!skill.latestVersionId) continue + const version = await ctx.db.get(skill.latestVersionId) + if (!version) continue + + const expected = { + version: version.version, + createdAt: version.createdAt, + changelog: version.changelog, + changelogSource: version.changelogSource, + clawdis: version.parsed?.clawdis, + } + + // Skip if already in sync + const existing = skill.latestVersionSummary + if ( + existing && + existing.version === expected.version && + existing.createdAt === expected.createdAt && + existing.changelog === expected.changelog && + existing.changelogSource === expected.changelogSource && + JSON.stringify(existing.clawdis ?? null) === JSON.stringify(expected.clawdis ?? null) + ) { + continue + } + + await ctx.db.patch(skill._id, { latestVersionSummary: expected }) + patched++ + } + + if (!isDone) { + await ctx.scheduler.runAfter( + 0, + internal.maintenance.backfillLatestVersionSummaryInternal, + { + cursor: continueCursor, + batchSize: args.batchSize, + }, + ) + } + + return { patched, isDone, scanned: page.length } + }, +}) + +/** + * Backfill `isSuspicious` on all skills. Cursor-based paginated mutation + * that self-schedules until done. + */ +export const backfillIsSuspiciousInternal = internalMutation({ + args: { + cursor: v.optional(v.string()), + batchSize: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const batchSize = clampInt(args.batchSize ?? 100, 10, 200) + const { page, continueCursor, isDone } = await ctx.db + .query('skills') + .paginate({ cursor: args.cursor ?? null, numItems: batchSize }) + + let patched = 0 + for (const skill of page) { + const expected = computeIsSuspicious(skill) + if (skill.isSuspicious !== expected) { + await ctx.db.patch(skill._id, { isSuspicious: expected }) + patched++ + } + } + + if (!isDone) { + await ctx.scheduler.runAfter(0, internal.maintenance.backfillIsSuspiciousInternal, { + cursor: continueCursor, + batchSize: args.batchSize, + }) + } + + return { patched, isDone, scanned: page.length } + }, +}) + function clampInt(value: number, min: number, max: number) { const rounded = Math.trunc(value) if (!Number.isFinite(rounded)) return min diff --git a/convex/schema.ts b/convex/schema.ts index 26bac1f5e0..5cd3e5b3e7 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -46,6 +46,15 @@ const skills = defineTable({ }), ), 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')), softDeletedAt: v.optional(v.number()), badges: v.optional( @@ -101,6 +110,7 @@ const skills = defineTable({ evaluatedAt: v.number(), }), ), + isSuspicious: v.optional(v.boolean()), moderationFlags: v.optional(v.array(v.string())), lastReviewedAt: v.optional(v.number()), // VT scan tracking @@ -146,6 +156,23 @@ const skills = defineTable({ ]) .index('by_canonical', ['canonicalSkillId']) .index('by_fork_of', ['forkOf.skillId']) + .index('by_moderation', ['moderationStatus', 'moderationReason']) + .index('by_nonsuspicious_updated', ['softDeletedAt', 'isSuspicious', 'updatedAt']) + .index('by_nonsuspicious_created', ['softDeletedAt', 'isSuspicious', 'createdAt']) + .index('by_nonsuspicious_name', ['softDeletedAt', 'isSuspicious', 'displayName']) + .index('by_nonsuspicious_downloads', [ + 'softDeletedAt', + 'isSuspicious', + 'statsDownloads', + 'updatedAt', + ]) + .index('by_nonsuspicious_stars', ['softDeletedAt', 'isSuspicious', 'statsStars', 'updatedAt']) + .index('by_nonsuspicious_installs', [ + 'softDeletedAt', + 'isSuspicious', + 'statsInstallsAllTime', + 'updatedAt', + ]) const souls = defineTable({ slug: v.string(), diff --git a/convex/skills.listPublicPageV2.test.ts b/convex/skills.listPublicPageV2.test.ts index b61a0174da..9658f4ec74 100644 --- a/convex/skills.listPublicPageV2.test.ts +++ b/convex/skills.listPublicPageV2.test.ts @@ -189,6 +189,67 @@ describe('skills.listPublicPageV2', () => { expect(paginateMock).toHaveBeenCalledTimes(1) }) + it('keeps nonSuspicious pagination on the base sort index while backfill is incomplete', async () => { + const suspicious = makeSkill( + 'skills:suspicious', + 'suspicious', + 'users:1', + 'skillVersions:1', + ['flagged.suspicious'], + ) + const cleanWithoutBackfill = makeSkill('skills:clean', 'clean', 'users:2', 'skillVersions:2') + const paginateMock = vi + .fn() + .mockResolvedValueOnce({ + page: [suspicious], + continueCursor: 'next-cursor', + isDone: false, + pageStatus: null, + splitCursor: null, + }) + .mockResolvedValueOnce({ + page: [cleanWithoutBackfill], + continueCursor: 'after-clean', + isDone: false, + pageStatus: null, + splitCursor: null, + }) + const withIndexMock = vi.fn(() => ({ + order: vi.fn(() => ({ paginate: paginateMock })), + })) + const ctx = { + db: { + query: vi.fn(() => ({ + withIndex: withIndexMock, + })), + get: vi.fn(async (id: string) => { + if (id.startsWith('users:')) return makeUser(id) + if (id.startsWith('skillVersions:')) return makeVersion(id) + return null + }), + }, + } + + const result = await listPublicPageV2Handler(ctx, { + paginationOpts: { cursor: null, numItems: 25 }, + sort: 'downloads', + dir: 'desc', + highlightedOnly: false, + nonSuspiciousOnly: true, + }) + + expect(result.page).toHaveLength(1) + expect(result.page[0]?.skill.slug).toBe('clean') + expect(result.continueCursor).toBe('after-clean') + expect(result.isDone).toBe(false) + expect(withIndexMock).toHaveBeenCalledTimes(2) + expect(withIndexMock).toHaveBeenNthCalledWith(1, 'by_active_stats_downloads', expect.any(Function)) + expect(withIndexMock).toHaveBeenNthCalledWith(2, 'by_active_stats_downloads', expect.any(Function)) + expect(withIndexMock).not.toHaveBeenCalledWith('by_nonsuspicious_downloads', expect.any(Function)) + expect(paginateMock).toHaveBeenNthCalledWith(1, { cursor: null, numItems: 25 }) + expect(paginateMock).toHaveBeenNthCalledWith(2, { cursor: 'next-cursor', numItems: 25 }) + }) + it('restarts pagination from first page when cursor is stale', async () => { const plain = makeSkill('skills:plain', 'plain', 'users:1', 'skillVersions:1') const paginateMock = vi @@ -339,6 +400,7 @@ function makeSkill( softDeletedAt: undefined, moderationStatus: 'active', moderationFlags, + moderationReason: undefined, } } diff --git a/convex/skills.ts b/convex/skills.ts index 8e88a78a7d..3ca5330bc7 100644 --- a/convex/skills.ts +++ b/convex/skills.ts @@ -54,7 +54,7 @@ import { publishVersionForUser, queueHighlightedWebhook, } from './lib/skillPublish' -import { isSkillSuspicious } from './lib/skillSafety' +import { computeIsSuspicious, isSkillSuspicious } from './lib/skillSafety' import { getFrontmatterValue, hashSkillFiles } from './lib/skills' export { publishVersionForUser } from './lib/skillPublish' @@ -574,13 +574,19 @@ 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 + const hasSummary = includeVersion && skill.latestVersionSummary const [latestVersionDoc, ownerInfo] = await Promise.all([ - includeVersion && skill.latestVersionId ? ctx.db.get(skill.latestVersionId) : null, + includeVersion && !hasSummary && skill.latestVersionId + ? ctx.db.get(skill.latestVersionId) + : null, getOwnerInfo(skill.ownerUserId), ]) const publicSkill = toPublicSkill(skill) if (!publicSkill) return null - const latestVersion = toPublicSkillListVersion(latestVersionDoc) + const latestVersion = hasSummary + ? toPublicSkillListVersionFromSummary(skill.latestVersionSummary!, skill.latestVersionId) + : toPublicSkillListVersion(latestVersionDoc) return { skill: publicSkill, latestVersion, @@ -608,6 +614,23 @@ function toPublicSkillListVersion( } } +function toPublicSkillListVersionFromSummary( + summary: NonNullable['latestVersionSummary']>, + latestVersionId: Id<'skillVersions'> | undefined, +): PublicSkillListVersion | null { + if (!latestVersionId) return null + return { + _id: latestVersionId, + // Approximates _creationTime; both are set to `now` in the same transaction + _creationTime: summary.createdAt, + version: summary.version, + createdAt: summary.createdAt, + changelog: summary.changelog, + changelogSource: summary.changelogSource, + parsed: summary.clawdis ? { clawdis: summary.clawdis } : undefined, + } +} + async function buildManagementSkillEntries(ctx: QueryCtx, skills: Doc<'skills'>[]) { const ownerCache = new Map, Promise | null>>() const badgeMapBySkillId = await getSkillBadgeMaps( @@ -1046,6 +1069,10 @@ export const clearOwnerSuspiciousFlagsInternal = internalMutation({ ) { patch.moderationStatus = 'active' } + patch.isSuspicious = computeIsSuspicious({ + moderationFlags: patch.moderationFlags, + moderationReason: (patch.moderationReason ?? skill.moderationReason) as string | undefined, + }) const nextSkill = { ...skill, ...patch } await ctx.db.patch(skill._id, patch) @@ -1619,6 +1646,10 @@ export const report = mutation({ moderationStatus: 'hidden', moderationReason: 'auto.reports', moderationNotes: 'Auto-hidden after 4 unique reports.', + isSuspicious: computeIsSuspicious({ + moderationFlags: skill.moderationFlags, + moderationReason: 'auto.reports', + }), hiddenAt: now, lastReviewedAt: now, }) @@ -1735,20 +1766,23 @@ export const listPublicPageV2 = query({ const dir = args.dir ?? (sort === 'name' ? 'asc' : 'desc') const { numItems, cursor: initialCursor } = normalizePublicListPagination(args.paginationOpts) - const runPaginate = (cursor: string | null) => - ctx.db + const runPaginate = (cursor: string | null) => { + return ctx.db .query('skills') .withIndex(SORT_INDEXES[sort], (q) => q.eq('softDeletedAt', undefined)) .order(dir) .paginate({ cursor, numItems }) + } // Use the index to filter out soft-deleted skills at query time. // softDeletedAt === undefined means active (non-deleted) skills only. // When post-pagination filters are active, skip empty filtered pages so clients // don't bounce between CanLoadMore/LoadingMore with no visible new rows. + // `isSuspicious` is still backfilled on existing rows, so mixing cursor families + // (`by_nonsuspicious_*` vs base sort indexes) can skip rows or duplicate pages. + // Stay on the base sort index and filter in JS until the backfill is complete. let result = await paginateWithStaleCursorRecovery(runPaginate, initialCursor) let filteredPage = filterPublicSkillPage(result.page, args) - while ((args.nonSuspiciousOnly || args.highlightedOnly) && filteredPage.length === 0 && !result.isDone) { result = await runPaginate(result.continueCursor) filteredPage = filterPublicSkillPage(result.page, args) @@ -2011,11 +2045,8 @@ export const getScanQueueHealthInternal = internalQuery({ handler: async (ctx) => { const pending = await ctx.db .query('skills') - .filter((q) => - q.and( - q.eq(q.field('moderationStatus'), 'hidden'), - q.eq(q.field('moderationReason'), 'pending.scan'), - ), + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'hidden').eq('moderationReason', 'pending.scan'), ) .collect() @@ -2057,23 +2088,31 @@ export const getActiveSkillsMissingVTCacheInternal = internalQuery({ // Skills waiting for VT + LLM-evaluated skills that still need VT cache const vtPending = await ctx.db .query('skills') - .filter((q) => - q.and( - q.eq(q.field('moderationStatus'), 'active'), - q.eq(q.field('moderationReason'), 'scanner.vt.pending'), - ), - ) - .take(poolSize) - const llmEvaluated = await ctx.db - .query('skills') - .filter((q) => - q.or( - q.eq(q.field('moderationReason'), 'scanner.llm.clean'), - q.eq(q.field('moderationReason'), 'scanner.llm.suspicious'), - q.eq(q.field('moderationReason'), 'scanner.llm.malicious'), - ), + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.vt.pending'), ) .take(poolSize) + const [llmClean, llmSuspicious, llmMalicious] = await Promise.all([ + ctx.db + .query('skills') + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.llm.clean'), + ) + .take(poolSize), + ctx.db + .query('skills') + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.llm.suspicious'), + ) + .take(poolSize), + ctx.db + .query('skills') + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.llm.malicious'), + ) + .take(poolSize), + ]) + const llmEvaluated = [...llmClean, ...llmSuspicious, ...llmMalicious] // Dedup across pools const seen = new Set() @@ -2120,7 +2159,7 @@ export const getAllActiveSkillsForRescanInternal = internalQuery({ handler: async (ctx) => { const activeSkills = await ctx.db .query('skills') - .filter((q) => q.eq(q.field('moderationStatus'), 'active')) + .withIndex('by_moderation', (q) => q.eq('moderationStatus', 'active')) .collect() const results: Array<{ @@ -2160,10 +2199,10 @@ export const getActiveSkillBatchForRescanInternal = internalQuery({ const batchSize = args.batchSize ?? 100 const cursor = args.cursor ?? 0 - // Query skills created after the cursor, ordered by _creationTime (ascending for stable pagination) + // Use built-in by_creation_time index for stable cursor-based pagination const candidates = await ctx.db .query('skills') - .filter((q) => q.gt(q.field('_creationTime'), cursor)) + .withIndex('by_creation_time', (q) => q.gt('_creationTime', cursor)) .order('asc') .take(batchSize * 3) // Over-fetch to account for filtering @@ -2218,9 +2257,10 @@ export const getActiveSkillBatchForLlmBackfillInternal = internalQuery({ const batchSize = args.batchSize ?? 10 const cursor = args.cursor ?? 0 + // Use built-in by_creation_time index for stable cursor-based pagination const candidates = await ctx.db .query('skills') - .filter((q) => q.gt(q.field('_creationTime'), cursor)) + .withIndex('by_creation_time', (q) => q.gt('_creationTime', cursor)) .order('asc') .take(batchSize * 3) @@ -2266,12 +2306,23 @@ export const getSkillsWithStaleModerationReasonInternal = internalQuery({ handler: async (ctx, args) => { const limit = args.limit ?? 100 - // Find skills with pending-like moderationReason - const staleReasons = new Set(['scanner.vt.pending', 'pending.scan']) - const allSkills = await ctx.db - .query('skills') - .filter((q) => q.eq(q.field('moderationStatus'), 'active')) - .collect() + // Over-fetch from each bucket since some will be filtered out (no vtAnalysis). + const poolSize = limit * 2 + // Find skills with pending-like moderationReason using indexed queries + const [vtPending, pendingScan] = await Promise.all([ + ctx.db + .query('skills') + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.vt.pending'), + ) + .take(poolSize), + ctx.db + .query('skills') + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'pending.scan'), + ) + .take(poolSize), + ]) const results: Array<{ skillId: Id<'skills'> @@ -2281,8 +2332,9 @@ export const getSkillsWithStaleModerationReasonInternal = internalQuery({ vtStatus: string | null }> = [] - for (const skill of allSkills) { - if (!skill.moderationReason || !staleReasons.has(skill.moderationReason)) continue + for (const skill of [...vtPending, ...pendingScan]) { + if (results.length >= limit) break + if (!skill.moderationReason) continue if (!skill.latestVersionId) continue const version = await ctx.db.get(skill.latestVersionId) @@ -2295,8 +2347,6 @@ export const getSkillsWithStaleModerationReasonInternal = internalQuery({ currentReason: skill.moderationReason, vtStatus: version.vtAnalysis.status, }) - - if (results.length >= limit) break } return results @@ -2314,11 +2364,8 @@ export const getPendingVTSkillsInternal = internalQuery({ const skills = await ctx.db .query('skills') - .filter((q) => - q.and( - q.eq(q.field('moderationStatus'), 'active'), - q.eq(q.field('moderationReason'), 'scanner.vt.pending'), - ), + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'scanner.vt.pending'), ) .take(limit) @@ -2355,8 +2402,13 @@ export const updateSkillModerationReasonInternal = internalMutation({ moderationReason: v.string(), }, handler: async (ctx, args) => { + const skill = await ctx.db.get(args.skillId) await ctx.db.patch(args.skillId, { moderationReason: args.moderationReason, + isSuspicious: computeIsSuspicious({ + moderationFlags: skill?.moderationFlags, + moderationReason: args.moderationReason, + }), }) }, }) @@ -2501,6 +2553,10 @@ export const applyBanToOwnedSkillsBatchInternal = internalMutation({ patch.hiddenAt = args.bannedAt patch.hiddenBy = args.hiddenBy patch.lastReviewedAt = args.bannedAt + patch.isSuspicious = computeIsSuspicious({ + moderationFlags: skill.moderationFlags, + moderationReason: 'user.banned', + }) hiddenCount += 1 } @@ -2550,6 +2606,10 @@ export const restoreOwnedSkillsForUnbanBatchInternal = internalMutation({ softDeletedAt: undefined, moderationStatus: 'active', moderationReason: 'restored.unban', + isSuspicious: computeIsSuspicious({ + moderationFlags: skill.moderationFlags, + moderationReason: 'restored.unban', + }), hiddenAt: undefined, hiddenBy: undefined, lastReviewedAt: now, @@ -2585,11 +2645,8 @@ export const getLegacyPendingScanSkillsInternal = internalQuery({ const limit = args.limit ?? 1000 const skills = await ctx.db .query('skills') - .filter((q) => - q.and( - q.eq(q.field('moderationStatus'), 'active'), - q.eq(q.field('moderationReason'), 'pending.scan'), - ), + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', 'pending.scan'), ) .take(limit) @@ -2624,11 +2681,8 @@ export const getUnscannedActiveSkillsInternal = internalQuery({ const limit = args.limit ?? 1000 const skills = await ctx.db .query('skills') - .filter((q) => - q.and( - q.eq(q.field('moderationStatus'), 'active'), - q.eq(q.field('moderationReason'), undefined), - ), + .withIndex('by_moderation', (q) => + q.eq('moderationStatus', 'active').eq('moderationReason', undefined), ) .take(limit) @@ -2681,6 +2735,10 @@ export const markScanStaleInternal = internalMutation({ await ctx.db.patch(args.skillId, { moderationReason: 'pending.scan.stale', + isSuspicious: computeIsSuspicious({ + moderationFlags: skill.moderationFlags, + moderationReason: 'pending.scan.stale', + }), updatedAt: Date.now(), }) }, @@ -2829,6 +2887,10 @@ export const approveSkillByHashInternal = internalMutation({ moderationReason: nextModerationReason, moderationFlags: newFlags, moderationNotes: nextModerationNotes, + isSuspicious: computeIsSuspicious({ + moderationFlags: newFlags, + moderationReason: nextModerationReason, + }), hiddenAt: nextModerationStatus === 'hidden' ? now : undefined, hiddenBy: undefined, lastReviewedAt: nextModerationStatus === 'hidden' ? now : undefined, @@ -2889,8 +2951,9 @@ export const escalateByVtInternal = internalMutation({ newFlags = ['flagged.suspicious'] } + const nextModerationFlags = newFlags.length ? newFlags : undefined const patch: Partial> = { - moderationFlags: newFlags.length ? newFlags : undefined, + moderationFlags: nextModerationFlags, updatedAt: Date.now(), } if (bypassSuspicious) { @@ -2904,6 +2967,11 @@ export const escalateByVtInternal = internalMutation({ patch.moderationStatus = 'hidden' } + patch.isSuspicious = computeIsSuspicious({ + moderationFlags: nextModerationFlags, + moderationReason: (patch.moderationReason ?? skill.moderationReason) as string | undefined, + }) + const nextSkill = { ...skill, ...patch } await ctx.db.patch(skill._id, patch) await adjustGlobalPublicCountForSkillChange(ctx, skill, nextSkill) @@ -3103,11 +3171,27 @@ export const updateTags = mutation({ const latestEntry = args.tags.find((entry) => entry.tag === 'latest') const now = Date.now() - await ctx.db.patch(skill._id, { + const patch: Partial> = { tags: nextTags, latestVersionId: latestEntry ? latestEntry.versionId : skill.latestVersionId, updatedAt: now, - }) + } + + // Keep latestVersionSummary in sync when the latest tag is repointed + if (latestEntry && latestEntry.versionId !== skill.latestVersionId) { + const version = await ctx.db.get(latestEntry.versionId) + if (version) { + patch.latestVersionSummary = { + version: version.version, + createdAt: version.createdAt, + changelog: version.changelog, + changelogSource: version.changelogSource, + clawdis: version.parsed?.clawdis, + } + } + } + + await ctx.db.patch(skill._id, patch) if (latestEntry) { await setSkillEmbeddingsLatestVersion(ctx, skill._id, latestEntry.versionId, now) @@ -3815,6 +3899,7 @@ export const insertVersion = internalMutation({ parsed: args.parsed, files: args.files, }) + const newSkillFlags = moderationFlags.length ? moderationFlags : undefined const skillId = await ctx.db.insert('skills', { slug: args.slug, displayName: args.displayName, @@ -3835,7 +3920,11 @@ export const insertVersion = internalMutation({ moderationReason, moderationNotes, quality: qualityRecord, - moderationFlags: moderationFlags.length ? moderationFlags : undefined, + moderationFlags: newSkillFlags, + isSuspicious: computeIsSuspicious({ + moderationFlags: newSkillFlags, + moderationReason: moderationReason, + }), reportCount: 0, lastReportedAt: undefined, statsDownloads: 0, @@ -3898,10 +3987,18 @@ export const insertVersion = internalMutation({ files: args.files, }) + const nextFlags = moderationFlags.length ? moderationFlags : undefined const patch: Partial> = { displayName: args.displayName, summary: nextSummary ?? undefined, latestVersionId: versionId, + latestVersionSummary: { + version: args.version, + createdAt: now, + changelog: args.changelog, + changelogSource: args.changelogSource, + clawdis: args.parsed.clawdis, + }, tags: nextTags, stats: { ...skill.stats, versions: skill.stats.versions + 1 }, softDeletedAt: undefined, @@ -3909,7 +4006,11 @@ export const insertVersion = internalMutation({ moderationReason, moderationNotes, quality: qualityRecord ?? skill.quality, - moderationFlags: moderationFlags.length ? moderationFlags : undefined, + moderationFlags: nextFlags, + isSuspicious: computeIsSuspicious({ + moderationFlags: nextFlags, + moderationReason: moderationReason, + }), updatedAt: now, } const nextSkill = { ...skill, ...patch } diff --git a/packages/schema/dist/schemas.d.ts b/packages/schema/dist/schemas.d.ts index 77bc5c09b9..1b883afa10 100644 --- a/packages/schema/dist/schemas.d.ts +++ b/packages/schema/dist/schemas.d.ts @@ -135,6 +135,16 @@ export declare const ApiV1WhoamiResponseSchema: import("arktype/internal/variant image?: string | null | undefined; }; }, {}>; +export declare const ApiV1UserSearchResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + items: { + userId: string; + handle: string | null; + displayName?: string | null | undefined; + name?: string | null | undefined; + role?: "user" | "admin" | "moderator" | null | undefined; + }[]; + total: number; +}, {}>; export declare const ApiV1SearchResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ results: { score: number; @@ -192,6 +202,12 @@ export declare const ApiV1SkillVersionListResponseSchema: import("arktype/intern }[]; nextCursor: string | null; }, {}>; +export declare const SecurityStatusSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + status: "clean" | "suspicious" | "malicious" | "pending" | "error"; + hasWarnings: boolean; + checkedAt: number | null; + model: string | null; +}, {}>; export declare const ApiV1SkillVersionResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{ version: { version: string; @@ -199,6 +215,12 @@ export declare const ApiV1SkillVersionResponseSchema: import("arktype/internal/v changelog: string; changelogSource?: "user" | "auto" | null | undefined; files?: unknown; + security?: { + status: "clean" | "suspicious" | "malicious" | "pending" | "error"; + hasWarnings: boolean; + checkedAt: number | null; + model: string | null; + } | undefined; } | null; skill: { slug: string; @@ -264,6 +286,27 @@ export declare const ClawdisRequiresSchema: import("arktype/internal/variants/ob config?: string[] | undefined; }, {}>; export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]; +export declare const EnvVarDeclarationSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + name: string; + required?: boolean | undefined; + description?: string | undefined; +}, {}>; +export type EnvVarDeclaration = (typeof EnvVarDeclarationSchema)[inferred]; +export declare const DependencyDeclarationSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + name: string; + type: "brew" | "go" | "pip" | "npm" | "cargo" | "apt" | "other"; + version?: string | undefined; + url?: string | undefined; + repository?: string | undefined; +}, {}>; +export type DependencyDeclaration = (typeof DependencyDeclarationSchema)[inferred]; +export declare const SkillLinksSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + homepage?: string | undefined; + repository?: string | undefined; + documentation?: string | undefined; + changelog?: string | undefined; +}, {}>; +export type SkillLinks = (typeof SkillLinksSchema)[inferred]; export declare const ClawdisSkillMetadataSchema: import("arktype/internal/variants/object.ts").ObjectType<{ always?: boolean | undefined; skillKey?: string | undefined; @@ -297,5 +340,40 @@ export declare const ClawdisSkillMetadataSchema: import("arktype/internal/varian stateDirs?: string[] | undefined; example?: string | undefined; } | undefined; + envVars?: { + name: string; + required?: boolean | undefined; + description?: string | undefined; + }[] | undefined; + dependencies?: { + name: string; + type: "brew" | "go" | "pip" | "npm" | "cargo" | "apt" | "other"; + version?: string | undefined; + url?: string | undefined; + repository?: string | undefined; + }[] | undefined; + author?: string | undefined; + links?: { + homepage?: string | undefined; + repository?: string | undefined; + documentation?: string | undefined; + changelog?: string | undefined; + } | undefined; }, {}>; -export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]; +export type ClawdisSkillMetadata = { + always?: boolean; + skillKey?: string; + primaryEnv?: string; + emoji?: string; + homepage?: string; + os?: string[]; + cliHelp?: string; + requires?: ClawdisRequires; + install?: SkillInstallSpec[]; + nix?: NixPluginSpec; + config?: ClawdbotConfigSpec; + envVars?: EnvVarDeclaration[]; + dependencies?: DependencyDeclaration[]; + author?: string; + links?: SkillLinks; +}; diff --git a/packages/schema/dist/schemas.js b/packages/schema/dist/schemas.js index 4da64dc272..ead0846c1d 100644 --- a/packages/schema/dist/schemas.js +++ b/packages/schema/dist/schemas.js @@ -110,6 +110,16 @@ export const ApiV1WhoamiResponseSchema = type({ image: 'string|null?', }, }); +export const ApiV1UserSearchResponseSchema = type({ + items: type({ + userId: 'string', + handle: 'string|null', + displayName: 'string|null?', + name: 'string|null?', + role: '"admin"|"moderator"|"user"|null?', + }).array(), + total: 'number', +}); export const ApiV1SearchResponseSchema = type({ results: type({ slug: 'string?', @@ -167,6 +177,12 @@ export const ApiV1SkillVersionListResponseSchema = type({ }).array(), nextCursor: 'string|null', }); +export const SecurityStatusSchema = type({ + status: '"clean" | "suspicious" | "malicious" | "pending" | "error"', + hasWarnings: 'boolean', + checkedAt: 'number|null', + model: 'string|null', +}); export const ApiV1SkillVersionResponseSchema = type({ version: type({ version: 'string', @@ -174,6 +190,7 @@ export const ApiV1SkillVersionResponseSchema = type({ changelog: 'string', changelogSource: '"auto"|"user"|null?', files: 'unknown?', + security: SecurityStatusSchema.optional(), }).or('null'), skill: type({ slug: 'string', @@ -231,6 +248,24 @@ export const ClawdisRequiresSchema = type({ env: 'string[]?', config: 'string[]?', }); +export const EnvVarDeclarationSchema = type({ + name: 'string', + required: 'boolean?', + description: 'string?', +}); +export const DependencyDeclarationSchema = type({ + name: 'string', + type: '"pip"|"npm"|"brew"|"go"|"cargo"|"apt"|"other"', + version: 'string?', + url: 'string?', + repository: 'string?', +}); +export const SkillLinksSchema = type({ + homepage: 'string?', + repository: 'string?', + documentation: 'string?', + changelog: 'string?', +}); export const ClawdisSkillMetadataSchema = type({ always: 'boolean?', skillKey: 'string?', @@ -243,5 +278,11 @@ export const ClawdisSkillMetadataSchema = type({ install: SkillInstallSpecSchema.array().optional(), nix: NixPluginSpecSchema.optional(), config: ClawdbotConfigSpecSchema.optional(), + envVars: EnvVarDeclarationSchema.array().optional(), + dependencies: DependencyDeclarationSchema.array().optional(), + author: 'string?', + links: SkillLinksSchema.optional(), }); +// If this line errors, ClawdisSkillMetadata is out of sync with ClawdisSkillMetadataSchema +const _clawdisKeysCheck = true; //# sourceMappingURL=schemas.js.map \ No newline at end of file diff --git a/packages/schema/dist/schemas.js.map b/packages/schema/dist/schemas.js.map index 694026c5c8..a06f94e44a 100644 --- a/packages/schema/dist/schemas.js.map +++ b/packages/schema/dist/schemas.js.map @@ -1 +1 @@ -{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index 1cd2c163ad..bcb5cb0b55 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -336,4 +336,27 @@ export const ClawdisSkillMetadataSchema = type({ author: 'string?', links: SkillLinksSchema.optional(), }) -export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred] +// Explicit interface because ArkType's [inferred] doesn't resolve all fields for TS. +// The _ClawdisSkillMetadataCheck below will fail to compile if this drifts from the schema. +export type ClawdisSkillMetadata = { + always?: boolean + skillKey?: string + primaryEnv?: string + emoji?: string + homepage?: string + os?: string[] + cliHelp?: string + requires?: ClawdisRequires + install?: SkillInstallSpec[] + nix?: NixPluginSpec + config?: ClawdbotConfigSpec + envVars?: EnvVarDeclaration[] + dependencies?: DependencyDeclaration[] + author?: string + links?: SkillLinks +} +type _ClawdisInferred = (typeof ClawdisSkillMetadataSchema)[inferred] +type _AssertExactKeys = [keyof A] extends [keyof B] ? [keyof B] extends [keyof A] ? true : never : never +type _ClawdisKeysMatch = _AssertExactKeys +// If this line errors, ClawdisSkillMetadata is out of sync with ClawdisSkillMetadataSchema +const _clawdisKeysCheck: _ClawdisKeysMatch = true