diff --git a/convex/canonicalTrending.test.ts b/convex/canonicalTrending.test.ts index 3369105c..3c8f5dc2 100644 --- a/convex/canonicalTrending.test.ts +++ b/convex/canonicalTrending.test.ts @@ -552,6 +552,39 @@ describe("canonical Trending snapshot storage", () => { ).toEqual({ status: "unavailable" }); }); + it("returns the current native-only snapshot for guarded preflight reuse", async () => { + const t = convexTest(schema, modules); + const now = Date.now(); + await t.mutation(internal.canonicalTrending.startSnapshotInternal, { + snapshotId: "skills-native-preflight-ready", + generatedAt: now - 1_000, + expiresAt: now + 24 * 60 * 60 * 1_000, + windowStartDay: 40, + windowEndDay: 40, + }); + await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, { + snapshotId: "skills-native-preflight-ready", + completedAt: now - 500, + totalItems: 0, + sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 }, + operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, + }); + + await expect( + t.query(internal.canonicalTrending.getReadyNativeSnapshotInternal, { now }), + ).resolves.toEqual({ + status: "ready", + snapshotId: "skills-native-preflight-ready", + generatedAt: new Date(now - 1_000).toISOString(), + windowHours: 24, + rankingVersion: "skills-trending-v2", + totalItems: 0, + sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 }, + operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, + reused: true, + }); + }); + it("never serves a snapshot produced by the legacy ranking algorithm", async () => { const t = convexTest(schema, modules); const now = Date.now(); diff --git a/convex/canonicalTrending.ts b/convex/canonicalTrending.ts index a1a6d077..0f05ce86 100644 --- a/convex/canonicalTrending.ts +++ b/convex/canonicalTrending.ts @@ -705,6 +705,41 @@ export const materializeInternal = internalAction({ }, }); +export const getReadyNativeSnapshotInternal = internalQuery({ + args: { now: v.number() }, + handler: async (ctx, args) => { + const snapshot = await ctx.db + .query("canonicalTrendingSnapshots") + .withIndex("by_kind_and_status_and_expires_at", (q) => + q.eq("kind", "skills").eq("status", "ready").gt("expiresAt", args.now), + ) + .order("desc") + .first(); + if ( + !snapshot || + snapshot.generatedAt + SNAPSHOT_MAX_SERVING_AGE_MS <= args.now || + snapshot.rankingVersion !== CANONICAL_TRENDING_RANKING_VERSION || + snapshot.totalItems === undefined || + !snapshot.sourceCounts || + snapshot.sourceCounts.skillsShTrending !== 0 || + !snapshot.operations + ) { + return null; + } + return { + status: "ready" as const, + snapshotId: snapshot.snapshotId, + generatedAt: new Date(snapshot.generatedAt).toISOString(), + windowHours: snapshot.windowHours, + rankingVersion: snapshot.rankingVersion, + totalItems: snapshot.totalItems, + sourceCounts: snapshot.sourceCounts, + operations: snapshot.operations, + reused: true as const, + }; + }, +}); + export const getPageInternal = internalQuery({ args: { cursor: v.union(v.string(), v.null()), diff --git a/convex/skillsShMirrorVisibility.test.ts b/convex/skillsShMirrorVisibility.test.ts index 52bf0c8a..6df3f1c5 100644 --- a/convex/skillsShMirrorVisibility.test.ts +++ b/convex/skillsShMirrorVisibility.test.ts @@ -616,6 +616,52 @@ describe("skills.sh mirror visibility operations", () => { ).rejects.toThrow("skills.sh mirror control is not active"); }); + it("reuses a fresh native-only snapshot instead of rematerializing preflight", async () => { + const t = convexTest(schema, modules); + const now = Date.now(); + await t.mutation(internal.canonicalTrending.startSnapshotInternal, { + snapshotId: "skills-native-preflight-existing", + generatedAt: now - 1_000, + expiresAt: now + 24 * 60 * 60 * 1_000, + windowStartDay: 40, + windowEndDay: 40, + }); + await t.mutation(internal.canonicalTrending.finalizeSnapshotInternal, { + snapshotId: "skills-native-preflight-existing", + completedAt: now - 500, + totalItems: 0, + sourceCounts: { clawhubTrending: 3, clawhubRising: 2, skillsShTrending: 0 }, + operations: { documentsRead: 10, documentsWritten: 2, functionCalls: 3 }, + }); + + await expect( + t.action(internal.skillsShMirrorVisibility.prepareNativeTrendingInternal, { + actor: "codex-test", + reason: "reuse current native-only Trending", + confirm: "deactivate-skills-sh-public-test", + }), + ).resolves.toMatchObject({ + ok: true, + nativeTrending: { + status: "ready", + snapshotId: "skills-native-preflight-existing", + sourceCounts: { skillsShTrending: 0 }, + reused: true, + }, + }); + const state = await t.run(async (ctx) => ({ + snapshots: await ctx.db.query("canonicalTrendingSnapshots").collect(), + control: await ctx.db + .query("skillsShMirrorControls") + .withIndex("by_key", (q) => q.eq("key", "global")) + .unique(), + })); + expect(state).toMatchObject({ + snapshots: [expect.objectContaining({ snapshotId: "skills-native-preflight-existing" })], + }); + expect(state.control?.activationLockToken).toBeUndefined(); + }); + it("rejects a completed Trending run older than the selected leaderboard import", async () => { const t = convexTest(schema, modules); await t.run(async (ctx) => { diff --git a/convex/skillsShMirrorVisibility.ts b/convex/skillsShMirrorVisibility.ts index ab555496..49d457ed 100644 --- a/convex/skillsShMirrorVisibility.ts +++ b/convex/skillsShMirrorVisibility.ts @@ -18,7 +18,7 @@ const MAX_ROWS = 50_000; const ACTIVATION_LOCK_LEASE_MS = 15 * 60_000; const internalRefs = internal as unknown as { - canonicalTrending: { materializeInternal: unknown }; + canonicalTrending: { getReadyNativeSnapshotInternal: unknown; materializeInternal: unknown }; skillsShMirrorVisibility: { beginActivationInternal: unknown; beginDeactivationInternal: unknown; @@ -499,7 +499,22 @@ export const beginNativeTrendingInternal = internalMutation({ }, }); -async function materializeNativeTrending(ctx: Pick, lockToken: string) { +async function materializeNativeTrending( + ctx: Pick, + lockToken: string, +) { + const reusable = (await ctx.runQuery( + internalRefs.canonicalTrending.getReadyNativeSnapshotInternal as never, + { now: Date.now() } as never, + )) as { + status: "ready"; + snapshotId: string; + sourceCounts: { clawhubTrending: number; clawhubRising: number; skillsShTrending: 0 }; + reused: true; + } | null; + // Native-only data is independent of skills.sh run chronology. The activation path + // always materializes its mixed snapshot after verifying the exact imported runs. + if (reusable) return reusable; const nativeTrending = (await ctx.runAction( internalRefs.canonicalTrending.materializeInternal as never, { activationLockToken: lockToken, skillsShMode: "native-only" } as never, @@ -515,7 +530,7 @@ async function materializeNativeTrending(ctx: Pick, lock } async function materializeNativeTrendingWithLock( - ctx: Pick, + ctx: Pick, args: { actor: string; reason: string; confirm: string }, ) { const lockToken = `skills-sh-native-trending:${crypto.randomUUID()}`;