From d8f82b7379885d5aa551485ebb22737082a859e3 Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 18 Jun 2026 15:52:46 -0700 Subject: [PATCH] chore: remove catalog classification rollout migrations (#2735) * chore: remove catalog classification rollout migrations * chore: keep static audit gate green --- convex/catalogClassification.ts | 13 -- convex/migrations.test.ts | 59 -------- convex/migrations.ts | 251 +------------------------------- specs/catalog-taxonomy.md | 23 +-- specs/deploy.md | 11 +- 5 files changed, 8 insertions(+), 349 deletions(-) delete mode 100644 convex/migrations.test.ts diff --git a/convex/catalogClassification.ts b/convex/catalogClassification.ts index 3f538276..a1e1357a 100644 --- a/convex/catalogClassification.ts +++ b/convex/catalogClassification.ts @@ -307,19 +307,6 @@ export const classifyCatalog: ReturnType = action({ }, }); -export const applyCatalogClassifications: ReturnType = action({ - args: { - dryRun: v.optional(v.boolean()), - minimumConfidence: v.union(v.literal("high"), v.literal("medium")), - confirm: v.optional(v.string()), - }, - handler: async (ctx, args) => { - const { user } = await requireUserFromAction(ctx); - assertRole(user, ["admin"]); - return ctx.runAction(internal.migrations.runCatalogClassificationApply, args); - }, -}); - export type StoredCatalogClassificationInput = ReturnType< typeof import("./lib/catalogClassification").prepareCatalogClassificationResult > & { diff --git a/convex/migrations.test.ts b/convex/migrations.test.ts deleted file mode 100644 index 0ca9646d..00000000 --- a/convex/migrations.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* @vitest-environment node */ - -import { describe, expect, it, vi } from "vitest"; -import { internal } from "./_generated/api"; -import { runCatalogClassificationApply, runCatalogTaxonomyPrerequisites } from "./migrations"; - -type WrappedHandler = { - _handler: (ctx: unknown, args: { dryRun?: boolean }) => Promise; -}; - -type ClassificationApplyWrappedHandler = { - _handler: ( - ctx: unknown, - args: { dryRun?: boolean; minimumConfidence: "high" | "medium"; confirm?: string }, - ) => Promise; -}; - -describe("catalog taxonomy migrations", () => { - it("dry-runs both tracked digest migrations", async () => { - const runMutation = vi.fn().mockResolvedValue({}); - const handler = (runCatalogTaxonomyPrerequisites as unknown as WrappedHandler)._handler; - - await handler({ runMutation }, { dryRun: true }); - - expect(runMutation).toHaveBeenNthCalledWith(1, internal.migrations.run, { - fn: "migrations:rebuildCatalogTaxonomyPackageDigests", - dryRun: true, - reset: true, - }); - expect(runMutation).toHaveBeenNthCalledWith(2, internal.migrations.run, { - fn: "migrations:rebuildCatalogTaxonomySkillDigests", - dryRun: true, - reset: true, - }); - }); - - it("dry-runs the selected classification apply migration", async () => { - const runMutation = vi.fn().mockResolvedValue({}); - const handler = (runCatalogClassificationApply as unknown as ClassificationApplyWrappedHandler) - ._handler; - - await handler({ runMutation }, { minimumConfidence: "medium" }); - - expect(runMutation).toHaveBeenCalledWith(internal.migrations.run, { - fn: "migrations:applyMediumConfidenceCatalogClassifications", - dryRun: true, - reset: true, - }); - }); - - it("requires an explicit confidence-specific confirmation before applying", async () => { - const handler = (runCatalogClassificationApply as unknown as ClassificationApplyWrappedHandler) - ._handler; - - await expect( - handler({ runMutation: vi.fn() }, { dryRun: false, minimumConfidence: "high" }), - ).rejects.toThrow('Pass confirm="apply-high-confidence-catalog-classifications" to apply.'); - }); -}); diff --git a/convex/migrations.ts b/convex/migrations.ts index 78756565..b93c8cd4 100644 --- a/convex/migrations.ts +++ b/convex/migrations.ts @@ -1,257 +1,10 @@ -import { Migrations, runToCompletion } from "@convex-dev/migrations"; -import { - normalizeInferredCatalogTopics, - normalizePluginCategories, - normalizeSkillCategories, -} from "clawhub-schema"; -import { ConvexError, v } from "convex/values"; -import { components, internal } from "./_generated/api"; -import type { Doc } from "./_generated/dataModel"; -import type { MutationCtx } from "./_generated/server"; -import { internalAction } from "./_generated/server"; -import { syncPackageSearchDigestForPackageId } from "./functions"; -import { - selectCatalogInference, - type CatalogClassificationConfidence, -} from "./lib/catalogClassification"; -import { syncSkillSearchDigestForSkill } from "./lib/skillSearchDigest"; +import { Migrations } from "@convex-dev/migrations"; +import { components } from "./_generated/api"; import schema from "./schema"; -const APPLY_HIGH_CONFIDENCE_CONFIRM = "apply-high-confidence-catalog-classifications"; -const APPLY_MEDIUM_CONFIDENCE_CONFIRM = "apply-medium-confidence-catalog-classifications"; - export const migrations = new Migrations(components.migrations, { schema, defaultBatchSize: 25, }); -export const rebuildCatalogTaxonomyPackageDigests = migrations.define({ - table: "packages", - migrateOne: async (ctx, pkg) => { - await syncPackageSearchDigestForPackageId(ctx, pkg._id); - }, -}); - -export const rebuildCatalogTaxonomySkillDigests = migrations.define({ - table: "skills", - migrateOne: async (ctx, skill) => { - await syncSkillSearchDigestForSkill(ctx, skill); - }, -}); - -async function applyCatalogClassification( - ctx: Pick, - result: Doc<"catalogClassificationResults">, - minimumConfidence: CatalogClassificationConfidence, -) { - const now = Date.now(); - if (result.targetKind === "skill" && result.skillId) { - const skill = await ctx.db.get(result.skillId); - if (!skill) { - await ctx.db.patch(result._id, { - applyStatus: "stale", - error: "Skill no longer exists", - appliedAt: undefined, - }); - return; - } - const selection = selectCatalogInference({ - currentSourceId: skill.latestVersionId, - resultSourceId: result.skillVersionId, - authorCategories: skill.categories, - authorTopics: skill.topics, - result: { - categories: result.categories, - topics: result.topics, - confidence: result.categoryConfidence, - topicConfidence: result.topicConfidence, - }, - minimumConfidence, - }); - if (selection.status !== "applied") { - await ctx.db.patch(result._id, { - applyStatus: selection.status, - error: undefined, - appliedAt: undefined, - }); - return; - } - const patch = { - inferredCategories: selection.categories - ? normalizeSkillCategories(selection.categories) - : undefined, - inferredTopics: selection.topics - ? normalizeInferredCatalogTopics(selection.topics) - : undefined, - inferredFromVersionId: result.skillVersionId, - inferredCategoryConfidence: selection.categories ? result.categoryConfidence : undefined, - inferredTopicConfidence: selection.topics ? result.topicConfidence : undefined, - inferredClassifierVersion: result.classifierVersion, - inferredTopicClassifierVersion: result.topicClassifierVersion, - inferredInputHash: result.inputHash, - inferredTopicInputHash: result.topicInputHash, - inferredAt: now, - }; - await ctx.db.patch(skill._id, patch); - await syncSkillSearchDigestForSkill(ctx, { ...skill, ...patch }); - await ctx.db.patch(result._id, { - applyStatus: "applied", - error: undefined, - appliedAt: now, - }); - return; - } - - if (result.targetKind === "plugin" && result.packageId) { - const pkg = await ctx.db.get(result.packageId); - if (!pkg) { - await ctx.db.patch(result._id, { - applyStatus: "stale", - error: "Package no longer exists", - appliedAt: undefined, - }); - return; - } - const selection = selectCatalogInference({ - currentSourceId: pkg.latestReleaseId, - resultSourceId: result.packageReleaseId, - authorCategories: pkg.categories, - authorTopics: pkg.topics, - result: { - categories: result.categories, - topics: result.topics, - confidence: result.categoryConfidence, - topicConfidence: result.topicConfidence, - }, - minimumConfidence, - }); - if (selection.status !== "applied") { - await ctx.db.patch(result._id, { - applyStatus: selection.status, - error: undefined, - appliedAt: undefined, - }); - return; - } - await ctx.db.patch(pkg._id, { - inferredCategories: selection.categories - ? normalizePluginCategories(selection.categories) - : undefined, - inferredTopics: selection.topics - ? normalizeInferredCatalogTopics(selection.topics) - : undefined, - inferredFromReleaseId: result.packageReleaseId, - inferredCategoryConfidence: selection.categories ? result.categoryConfidence : undefined, - inferredTopicConfidence: selection.topics ? result.topicConfidence : undefined, - inferredClassifierVersion: result.classifierVersion, - inferredTopicClassifierVersion: result.topicClassifierVersion, - inferredInputHash: result.inputHash, - inferredTopicInputHash: result.topicInputHash, - inferredAt: now, - }); - await syncPackageSearchDigestForPackageId(ctx, pkg._id); - await ctx.db.patch(result._id, { - applyStatus: "applied", - error: undefined, - appliedAt: now, - }); - return; - } - - await ctx.db.patch(result._id, { - applyStatus: "error", - error: "Classification target is inconsistent", - appliedAt: undefined, - }); -} - -export const applyHighConfidenceCatalogClassifications = migrations.define({ - table: "catalogClassificationResults", - batchSize: 10, - migrateOne: (ctx, result) => applyCatalogClassification(ctx, result, "high"), -}); - -export const applyMediumConfidenceCatalogClassifications = migrations.define({ - table: "catalogClassificationResults", - batchSize: 10, - migrateOne: (ctx, result) => applyCatalogClassification(ctx, result, "medium"), -}); - export const run = migrations.runner(); - -export const runCatalogClassificationApply = internalAction({ - args: { - dryRun: v.optional(v.boolean()), - minimumConfidence: v.union(v.literal("high"), v.literal("medium")), - confirm: v.optional(v.string()), - }, - returns: v.object({ - ok: v.literal(true), - dryRun: v.boolean(), - minimumConfidence: v.union(v.literal("high"), v.literal("medium")), - confirmRequired: v.optional(v.string()), - }), - handler: async (ctx, args) => { - const dryRun = args.dryRun !== false; - const confirmRequired = - args.minimumConfidence === "high" - ? APPLY_HIGH_CONFIDENCE_CONFIRM - : APPLY_MEDIUM_CONFIDENCE_CONFIRM; - if (!dryRun && args.confirm !== confirmRequired) { - throw new ConvexError(`Pass confirm="${confirmRequired}" to apply.`); - } - const migration = - args.minimumConfidence === "high" - ? internal.migrations.applyHighConfidenceCatalogClassifications - : internal.migrations.applyMediumConfidenceCatalogClassifications; - if (dryRun) { - await ctx.runMutation(internal.migrations.run, { - fn: - args.minimumConfidence === "high" - ? "migrations:applyHighConfidenceCatalogClassifications" - : "migrations:applyMediumConfidenceCatalogClassifications", - dryRun: true, - reset: true, - }); - } else { - await runToCompletion(ctx, components.migrations, migration); - } - return { - ok: true as const, - dryRun, - minimumConfidence: args.minimumConfidence, - confirmRequired: dryRun ? confirmRequired : undefined, - }; - }, -}); - -export const runCatalogTaxonomyPrerequisites = internalAction({ - args: { dryRun: v.optional(v.boolean()) }, - returns: v.null(), - handler: async (ctx, args): Promise => { - if (args.dryRun) { - for (const fn of [ - "migrations:rebuildCatalogTaxonomyPackageDigests", - "migrations:rebuildCatalogTaxonomySkillDigests", - ]) { - await ctx.runMutation(internal.migrations.run, { - fn, - dryRun: true, - reset: true, - }); - } - return null; - } - await runToCompletion( - ctx, - components.migrations, - internal.migrations.rebuildCatalogTaxonomyPackageDigests, - ); - await runToCompletion( - ctx, - components.migrations, - internal.migrations.rebuildCatalogTaxonomySkillDigests, - ); - return null; - }, -}); diff --git a/specs/catalog-taxonomy.md b/specs/catalog-taxonomy.md index dee3ebd6..bfad2189 100644 --- a/specs/catalog-taxonomy.md +++ b/specs/catalog-taxonomy.md @@ -22,9 +22,8 @@ - Capability tags are not taxonomy inputs. - Existing items without valid stored categories remain in `other` until an author or operator explicitly accepts generated or manually selected categories. -- Operators run the tracked package and skill catalog digest migrations after backend deployment - and verify completion before considering the taxonomy rollout complete. The deploy workflow does - not invoke them automatically. +- The one-time package and skill catalog digest migrations completed during the production rollout. + No catalog taxonomy migration remains in the backend deploy checklist. ## Topics @@ -67,19 +66,5 @@ Corpus classification is a separate operator-run phase from digest projection: - The preview runner is cursor-batched and resumable. It uses an action instead of `@convex-dev/migrations` because it must read immutable storage blobs; the source-changing apply phase uses the migrations component. -- Apply defaults to a dry run and requires a confidence-specific confirmation string. High - confidence is the default production lane. Medium confidence requires an explicit operator - decision after reviewing the generated corpus report. - -Operator sequence: - -```bash -bunx convex run catalogClassificationNode:classifyCatalogInternal \ - '{"targetKind":"skill","maxBatches":20,"continueOnIncomplete":true}' --prod -bunx convex run catalogClassificationNode:classifyCatalogInternal \ - '{"targetKind":"plugin","maxBatches":20,"continueOnIncomplete":true}' --prod -bunx convex run migrations:runCatalogClassificationApply \ - '{"minimumConfidence":"high","dryRun":true}' --prod -bunx convex run migrations:runCatalogClassificationApply \ - '{"minimumConfidence":"high","dryRun":false,"confirm":"apply-high-confidence-catalog-classifications"}' --prod -``` +- High- and medium-confidence rollout apply was a one-time production migration. The temporary + apply migrations and operator wrappers were removed after the verified rollout completed. diff --git a/specs/deploy.md b/specs/deploy.md index 65d9b5dc..8ac18a00 100644 --- a/specs/deploy.md +++ b/specs/deploy.md @@ -32,15 +32,8 @@ Production deploy notes: - `deploy.yml` is manual-only (`workflow_dispatch`). Merging to `main` does not deploy. - The workflow must be started from `main`. -- Backend deploys do not run the tracked catalog-taxonomy digest migrations automatically. For the - taxonomy rollout, an operator must run - `bunx convex run migrations:runCatalogTaxonomyPrerequisites --prod` after Convex deploy and verify - completion before considering the deployment complete. The migration is idempotent, resumable, - and a no-op after it completes. -- Catalog classification/backfill is also operator-run. Generate skill and plugin preview rows with - `catalogClassificationNode:classifyCatalogInternal`, review the resulting confidence lanes, then - dry-run and explicitly confirm `migrations:runCatalogClassificationApply`. See - `specs/catalog-taxonomy.md` for the exact commands and confidence-specific confirmation strings. +- The catalog-taxonomy digest and high-/medium-confidence classification rollout migrations were + one-time production operations and are no longer part of the deploy checklist. - Deploy targets: - `full`: deploy Convex, verify contract, wait for the matching Vercel production deploy, then run smoke tests - `backend`: deploy Convex, verify contract, then run smoke tests against current production