# Automated Submission Review Plan

## Current Plan: Reviewer Follow-Up Before Merge

### Scope Decisions

- Treat the reviewer’s missing `DELETE /api/admin/[id]` blocker as stale after the latest merge: the route now exports both `PATCH` and `DELETE` in `src/app/api/admin/[id]/route.ts`.
- Keep the 3072-dimensional Gemini image embedding migration. This is required for image-aware automated review and should not be rolled back to the prior text-only embedding size.
- Do not add env overrides for `PETDEX_EMBEDDING_MODEL` or the review model in this PR. Keep the project constants hardcoded as already decided.
- Do not move submission review fully async in this PR. Keep the synchronous bounded review path, add latency observability, and defer queue/polling architecture until there is real latency data.

### Files To Modify

- `drizzle/0007_submission_review_indexes.sql` or the next available migration number: add non-destructive performance indexes.
- `src/lib/db/schema.ts`: mirror supported non-vector indexes where Drizzle supports them.
- `src/lib/mock/db.ts`: add mock/PGlite fixups for any new non-vector columns/index-adjacent schema assumptions only if needed.
- `src/lib/submission-review.ts`: make `force` meaningful and tighten visual match candidate loading.
- `src/lib/submission-review-decision.ts`: raise auto-approve confidence from `0.9` to `0.95` for the first live week.
- `scripts/review-pending-submissions.ts`: make reruns idempotent by default and add explicit `--force` behavior.
- `src/lib/submissions.ts`: add canonical owner profile info to successful submission responses and keep review timing bounded.
- `src/app/api/submit/route.ts`: return the richer success response unchanged from `persistSubmission()`.
- `src/app/api/cli/submit/register/route.ts`: return the richer success response unchanged from `persistSubmission()`.
- `src/components/pet-submit-form.tsx`: add register/review duration analytics without exposing PII or raw pet content.
- `packages/petdex-cli/bin/petdex.ts`: replace stale `/my-pets` output with returned `/u/<handle>` profile URL.
- PR description or a short repo note near the review/backfill docs: document the required embedding backfill after `0005` in any environment where `0004` already populated 1536-dim embeddings.

### Database And Performance Changes

- Add a new migration after `0006`; do not edit the already-created `0005_gemini_embeddings.sql` migration except possibly comments if absolutely necessary.
- Add `submitted_pets` partial/composite index for visual review candidates, e.g. rows with `status IN ('approved', 'pending')` and `dhash IS NOT NULL`.
- Add `submitted_pets(status, created_at DESC)` for pending-review backfill ordering.
- Add `submission_reviews(submitted_pet_id, created_at DESC)` for latest-review lookups.
- Do not add a pgvector ANN index in this pass unless the exact operator class and 3072-dimensional behavior are verified locally; this PR only needs to fix the visual scan blocker.
- Keep `findVisualMatches()` JS Hamming distance for correctness, but stop treating unbounded full-table scans as acceptable:
  - query candidates in deterministic chunks ordered by newest/featured or newest first;
  - compute Hamming per chunk;
  - stop once enough threshold-passing matches have been collected;
  - cap total candidates scanned with a conservative limit and record a hold/manual-review reason if the cap is hit before the scan completes.
- Keep exact hash duplicate checks as the strongest fast path; those already have dedicated hash indexes from `0004`.

### Backfill Idempotency

- Change `reviewSubmission(submissionId, options)` so `force` is consumed.
- Default behavior: if the latest review for a submission is already `completed` or `failed`, return/reuse that review instead of inserting a duplicate.
- `force: true`: insert a fresh review and supersede prior evidence intentionally.
- Update `scripts/review-pending-submissions.ts`:
  - remove unconditional `{ force: true }`;
  - add `--force` parsing;
  - log when an existing review is reused/skipped;
  - optionally add a lightweight janitor step for very old `running` reviews, marking them failed/hold before processing.

### Submit Latency Observability

- Keep `/api/submit` and `/api/cli/submit/register` synchronous for now because the UI/CLI response shape already depends on immediate review outcome.
- In `src/components/pet-submit-form.tsx`, measure at least:
  - upload duration;
  - register/review duration;
  - total submission duration.
- Add Vercel Analytics fields with coarse, non-PII values:
  - `duration_ms` or bucketed duration;
  - `register_duration_ms` or bucket;
  - `review_decision`;
  - `review_applied`;
  - failure `stage` and `error_code` as already done.
- Do not expose duplicate/policy evidence details to users beyond existing safe summaries.

### CLI Profile URL Fix

- Add `profileHandle` and `profileUrl` to successful `persistSubmission()` results using `handleForUser(principal.userId)` from `src/lib/handles.ts`.
- Include those fields in web and CLI submit/register responses.
- Update `packages/petdex-cli/bin/petdex.ts` `SubmitOneResult` to read `profileUrl`.
- Replace both stale `/my-pets` messages with `profileUrl`, falling back to `${PETDEX_URL}/` if missing.
- Keep CLI exit semantics: held/rejected review outcomes are successful command exits if upload/register completed.

### Documentation / PR Notes

- Add a clear reviewer response that `DELETE /api/admin/[id]` is present post-merge and was verified.
- Add a migration/runbook note:
  - `0005` intentionally nulls old 1536-dimensional embeddings because the new review uses Gemini image-aware 3072-dimensional embeddings.
  - Any environment that ran `0004` with populated embeddings must rerun `scripts/compute-similarity.ts` or the equivalent embedding backfill after applying `0005`.
- Mention the sync-review choice explicitly: shipping synchronous bounded review now with timing analytics; async worker/polling is deferred until measurements justify it.

### Verification

- Confirm no conflict regressions:
  - `grep -R "<<<<<<<\|=======\|>>>>>>>" src drizzle scripts packages/petdex-cli || true`
- Focused tests:
  - `bun test --env-file=.env.mock src/lib/embeddings.test.ts src/lib/submission-review.test.ts src/lib/security.test.ts`
  - add/update tests for `reviewSubmission()` idempotency and `force: true` behavior if practical with existing test seams.
- i18n and build:
  - `bun run i18n:check`
  - `bun --env-file=.env.mock run build`
- Formatting/lint:
  - targeted `bunx biome check` on all modified root files.
  - note that full `bun run check` currently fails on unrelated repo-wide Biome issues unless those are fixed separately.
- CLI package:
  - from `packages/petdex-cli`, run `bun run build && bun run typecheck`.
- Manual QA:
  - `bun run dev:mock` and submit a sample pet; confirm success panel still renders and analytics calls do not throw.
  - Verify CLI output no longer references `/my-pets`.

## Archived Prior Plan

The notes below describe the initial automated-review rollout and are superseded by the reviewer follow-up plan above.

## Current Plan: Immediate Automated Decisions

### Product Decisions

- Dry-run mode is no longer part of the live submission flow.
- Automated review runs immediately after assets are uploaded and the submission row is created.
- Web and CLI submissions both receive the automated review outcome in the registration response.
- User-facing outcomes are immediate:
  - Approved: tell the user the pet is live and link to the pet page.
  - Rejected: tell the user the submission was rejected, with safe/generic reason copy.
  - Hold: tell the user the submission needs manual review and remains in the queue.
- If automated review errors or cannot finish within the bounded request window, fail closed as hold/manual review.
- Keep `submitted_pets.status = "pending"` for holds; do not add a new status enum value. The latest `submission_reviews.decision = "hold"` is the hold signal.

### Server Flow

- Update `src/lib/submissions.ts` so `persistSubmission()` awaits automated review instead of fire-and-forget triggering it.
- Return a richer success result from `persistSubmission()`:
  - `id`
  - `slug`
  - `status`: current `submitted_pets.status` after review (`pending`, `approved`, `rejected`)
  - `review`: normalized review outcome for clients
- Add a small adapter in `src/lib/submissions.ts` that converts `ReviewSubmissionResult` into client-safe output:
  - `decision`: `approved` | `rejected` | `hold`
  - `applied`: boolean
  - `reasonCode`: string | null
  - `summary`: string | null
- Add a bounded review timeout in `src/lib/submissions.ts`, e.g. `SUBMISSION_REVIEW_TIMEOUT_MS` with default `15000`.
- On timeout/error, write or update a `submission_reviews` row as `failed`/`hold` where possible, leave `submitted_pets.status = "pending"`, and return hold feedback.
- Keep admin notification email fire-and-forget, but avoid sending the old generic admin-review notification before an immediate auto-approve/auto-reject if that creates noisy false positives. If keeping it for v1, document that it is operational-only.

### Review Engine Changes

- Update `src/lib/submission-review.ts`:
  - Remove `dryRun` from `ReviewSubmissionOptions` and `submissionReviewFlags()` runtime behavior.
  - Create review records with `dryRun: false` while the column remains in the DB for existing schema compatibility.
  - Set `checks.autopilot.dryRun = false` until the JSON shape is migrated or simplified.
  - Keep `SUBMISSION_REVIEW_ENABLED` only as an emergency kill switch if needed; disabled mode should return hold/manual review, never silently skip review while telling the user success.
- Update `src/lib/submission-review-decision.ts`:
  - Remove `dryRun` from `ReviewFlags`.
  - Remove the `applyReason: "dry_run"` branch.
  - Keep the explicit `SUBMISSION_REVIEW_AUTO_APPROVE_ENABLED` and `SUBMISSION_REVIEW_AUTO_REJECT_ENABLED` gates only if we still want operational brakes. For this rollout, set live envs to enable both intended auto actions.
  - Holds remain non-applying decisions.
- Update `src/lib/submission-review-types.ts` only if we choose to remove `checks.autopilot.dryRun` from JSON. Minimal implementation can leave it as `false` for compatibility.

### API Response Changes

- Update `src/app/api/submit/route.ts` to return the new `persistSubmission()` result:
  - `ok: true`
  - `id`
  - `slug`
  - `status`
  - `review`
- Update `src/app/api/cli/submit/register/route.ts` with the same response shape.
- Keep auth, same-origin, bearer-token, and body-validation invariants unchanged.

### Web UX

- Update `src/components/pet-submit-form.tsx`:
  - Extend `SubmissionResult.success` with `status` and `review` fields.
  - Parse the new `/api/submit` response shape.
  - Track analytics with review outcome (`approved`, `rejected`, `hold`) in addition to slug.
  - Render distinct success panels:
    - Approved: positive/live message and pet page link.
    - Rejected: rejection message using generic/safe copy; avoid exposing duplicate/policy evidence details.
    - Hold: manual-review message using summary when safe, otherwise generic review copy.
- Update i18n messages in:
  - `src/i18n/messages/en.json`
  - `src/i18n/messages/es.json`
  - `src/i18n/messages/zh.json`
- Keep the existing upload/register progress steps; the `registering` step now includes automated review and may take longer.

### CLI UX

- Update `packages/petdex-cli/bin/petdex.ts`:
  - Parse `review` from `/api/cli/submit/register`.
  - Approved: print live URL/slug.
  - Rejected: print rejected message with safe reason.
  - Hold: print manual-review message.
  - Treat hold as exit code `0` because the submission was accepted and queued.
  - Treat rejected as a successful command if upload/register completed, unless current CLI semantics require non-zero for unpublished submissions. Recommended: exit `0` and communicate the outcome clearly.

### Admin/Internal Cleanup

- Update `src/app/api/internal/submissions/[id]/review/route.ts`:
  - Remove request-body `dryRun` handling.
  - Return `dryRun: false` only if keeping the response field for compatibility, or remove it from response consumers.
- Update `scripts/review-pending-submissions.ts`:
  - Remove `--dry` behavior for this review script.
  - Keep `--limit` and `--concurrency`.
  - Make the script name/output clear that it applies decisions.
- Update `src/components/admin-review-row.tsx`:
  - Remove “Would approve/reject” and “Dry-run only” labels.
  - Keep “Auto approve”, “Auto reject”, “Held”, and “Review failed”.
- Update `src/lib/db/schema.ts` and `src/lib/mock/db.ts` only for defaults if desired:
  - Minimal: keep `submission_reviews.dry_run` as an existing compatibility column but always write `false`.
  - Optional later migration: drop the column and remove `checks.autopilot.dryRun` from JSON.

### Verification

- Unit tests:
  - Update `src/lib/submission-review.test.ts` to remove dry-run expectations and prove clean submissions can apply when action gates are enabled.
  - Add/extend tests for exact duplicate auto-reject, policy hold, review failure hold, and low-confidence hold.
- API/persistence tests where practical:
  - `persistSubmission()` returns `approved`, `rejected`, or `hold` review outcomes.
  - Review error/timeout returns hold and leaves `submitted_pets.status = "pending"`.
  - `/api/submit` and `/api/cli/submit/register` include `status` and `review` in success responses.
- UI/manual verification:
  - Run `bun run dev:mock` for basic form rendering where possible.
  - On real-service dev, upload a known clean pack and confirm immediate approved feedback.
  - Upload a duplicate pack and confirm immediate rejected feedback.
  - Force a model/API failure and confirm hold/manual-review feedback.
  - Confirm `/admin` still shows held review evidence and approved/rejected rows correctly.
- Commands:
  - `bun test src/lib/submission-review.test.ts src/lib/embeddings.test.ts`
  - `bun run i18n:check`
  - Targeted Biome check for modified files, then `bun run check` if unrelated repo lint state allows it.
  - `bun run build`
  - For CLI package after edits: from `packages/petdex-cli`, run `bun run build && bun run typecheck` if dependencies are installed.

### Rollout Notes

- Set production/staging envs so review applies:
  - Remove `SUBMISSION_REVIEW_DRY_RUN` usage.
  - Ensure `SUBMISSION_REVIEW_AUTO_APPROVE_ENABLED=true` and `SUBMISSION_REVIEW_AUTO_REJECT_ENABLED=true` if keeping action gates.
  - Keep `SUBMISSION_REVIEW_ENABLED` only as an emergency switch; disabled mode must return hold/manual review.
- Monitor after deploy:
  - Upload/register latency.
  - Review timeout/error hold rate.
  - Auto-approval rate.
  - Auto-rejection rate.
  - User support reports for false rejects.
- If synchronous review latency is too high, the follow-up architecture is a review job plus client polling/SSE, but the recommended v1 implementation is synchronous bounded review because the requirement is immediate feedback.

## Archived Original Dry-Run Planning Notes

The notes below describe the initial dry-run rollout and are superseded by the current immediate automated-decision plan above.

## Decisions From Planning

- Launch shape: autopilot with holds.
- Duplicate enforcement: auto-reject only exact or near-exact duplicates.
- Policy enforcement: policy/sensitive-content flags hold for admin review in v1, never auto-reject.
- Rollout: dry-run first, then enable exact-duplicate reject and high-confidence auto-approve behind separate flags.
- Policy source of truth: add an explicit in-repo policy config used by the classifier prompt and admin UI.

## Recommended Architecture

- Keep `submitted_pets.status` as the public workflow state: `pending`, `approved`, `rejected`.
- Add automated review as an auditable first-pass layer, not as a replacement for the admin queue.
- Run review after a submission is persisted, outside the request-critical path.
- Reuse the existing manual approval/rejection side effects instead of duplicating them in the review job.
- Fail closed: if any asset, model, image decode, embedding, or database step fails, keep the submission `pending` and show the failure in admin.

## Files To Modify

- `src/lib/db/schema.ts`: add review-related tables/enums and optional exact-asset hash columns.
- `drizzle/0004_submission_reviews.sql`: add SQL migration for review records, asset hashes, and missing pgvector setup if needed.
- `src/lib/submission-decisions.ts`: extract reusable approve/reject/revive/edit behavior from `src/app/api/admin/[id]/route.ts`.
- `src/app/api/admin/[id]/route.ts`: delegate to shared decision helpers while preserving current admin API behavior.
- `src/lib/submission-review-policy.ts`: define typed v1 policy categories, severity, and admin labels.
- `src/lib/submission-review.ts`: orchestrate asset checks, policy checks, duplicate checks, decision writing, and optional status application.
- `src/lib/similarity.ts`: expose reusable dHash, text embedding, hamming distance, and vector persistence helpers for pending submissions.
- `src/app/api/internal/submissions/[id]/review/route.ts`: protected internal rerun/trigger endpoint for review execution.
- `scripts/review-pending-submissions.ts`: dry-run/backfill/retry script with `--dry`, `--limit`, `--force`, and bounded concurrency.
- `src/lib/submissions.ts`: after `persistSubmission` succeeds, trigger review best-effort when enabled.
- `src/lib/db/queries.ts`: add a query that returns submissions with latest review evidence for the admin page.
- `src/app/[locale]/admin/page.tsx`: add review-aware filtering and pass latest review metadata into rows.
- `src/components/admin-review-row.tsx`: add automation badge, evidence panel, and rerun review action.
- `src/lib/submission-review.test.ts`: unit-test duplicate, policy, dry-run, and final decision matrices.
- `src/lib/security.test.ts`: add asset-review regressions if helper functions live near existing validation/security utilities.
- `src/lib/mock/db.ts`: update mock/PGlite schema fixups so `bun run dev:mock` keeps working after schema additions.

## Data Model

- Add `submission_reviews` with `id`, `submitted_pet_id`, `status`, `decision`, `reason_code`, `summary`, `confidence`, `checks`, `model`, `dry_run`, `created_at`, `reviewed_at`, and `error`.
- Use `status`: `running`, `completed`, `failed`.
- Use `decision`: `auto_approve`, `auto_reject`, `hold`, `no_decision`.
- Store `checks` as JSONB with `assets`, `policy`, `duplicates`, and `autopilot` sections.
- Add indexes on `submitted_pet_id`, `decision`, `status`, and `created_at`.
- Add exact-hash columns to `submitted_pets`: `sprite_sha256`, `pet_json_sha256`, `zip_sha256`.
- Add indexes on exact-hash columns for fast duplicate lookup.
- If pgvector is not reliably migrated, add `CREATE EXTENSION IF NOT EXISTS vector` and `ALTER TABLE submitted_pets ADD COLUMN IF NOT EXISTS embedding vector(1536)` to the migration.

## Review Algorithm

- Step 1: fetch the submitted pet row and skip if it no longer exists.
- Step 2: fetch the public R2/legacy asset URLs only after `isAllowedAssetUrl` passes.
- Step 3: compute SHA-256 hashes for zip, sprite, and pet JSON.
- Step 4: parse and sanity-check `pet.json` and ZIP entries.
- Step 5: compute dHash from the first sprite frame using the existing `192x208` crop convention.
- Step 6: compute text embedding from display name, description, kind, tags, and vibes.
- Step 7: classify public-facing text and a sprite-frame image/contact sheet against the in-repo policy categories.
- Step 8: compare exact hashes, dHash, semantic embedding, slug/name, owner, and credit metadata against approved and pending submissions.
- Step 9: write a `submission_reviews` record with evidence regardless of whether statuses are mutated.
- Step 10: apply an automatic status change only if flags permit and the decision matrix is unambiguous.

## Duplicate Decision Rules

- Auto-reject when `sprite_sha256`, `pet_json_sha256`, or `zip_sha256` exactly matches an approved submission.
- Auto-reject when dHash distance is `0` against an approved submission.
- Auto-reject when dHash distance is `1..3` and at least one strong corroborating signal matches, such as normalized display name, slug base, same owner, or semantic score `>= 0.95`.
- Hold when dHash distance is `1..3` without corroboration.
- Hold when dHash distance is `4..6`.
- Hold when dHash distance is `7..14` plus metadata or semantic overlap.
- Hold when semantic score is `>= 0.88` without exact visual evidence.
- Never auto-reject same-character, derivative, or similar-style submissions based on text/embedding alone.
- Pass duplicate checks when there are no exact hashes, no dHash hit `<= 6`, and semantic score is below `0.88`.

## Policy Decision Rules

- Classify only public-facing content by default: display name, description, tags, vibes, credit name/URL, pet JSON labels, and sprite frame imagery.
- Categories should include sexual content, sexual minors, hate/harassment, graphic violence/gore, self-harm, illegal activity/drugs, personal data/doxxing, spam/scam, malware/script abuse, and explicit profanity/adult language.
- Copyright/trademark/same-character risk should be a hold signal, not an auto-reject signal.
- Any policy flag above low confidence holds the submission for admin review.
- Any classifier error, timeout, malformed response, or low confidence holds the submission.
- Only a clean policy result can contribute to auto-approval.

## Autopilot Matrix

- Dry-run mode always writes review records and never mutates `submitted_pets.status`.
- Auto-approve only when assets pass, policy passes, duplicate checks pass, all required checks complete, confidence is at least `0.9`, and `SUBMISSION_REVIEW_AUTO_APPROVE_ENABLED=true`.
- Auto-reject only for exact or near-exact duplicate rules and only when `SUBMISSION_REVIEW_AUTO_REJECT_ENABLED=true`.
- Hold for policy flags, model failures, asset warnings, image decode failures, semantic-only duplicates, near duplicates, slug/name collisions, or low confidence.
- Pending held submissions remain in the existing admin queue with review evidence.

## Runtime And Flags

- Add `SUBMISSION_REVIEW_ENABLED` to enable best-effort review triggering.
- Add `SUBMISSION_REVIEW_DRY_RUN` and default it to true.
- Add `SUBMISSION_REVIEW_AUTO_APPROVE_ENABLED` and default it to false.
- Add `SUBMISSION_REVIEW_AUTO_REJECT_ENABLED` and default it to false.
- Add `SUBMISSION_REVIEW_INTERNAL_SECRET` for the internal review route if it is callable over HTTP.
- Keep `OPENAI_API_KEY` optional; missing key should hold submissions, not fail submission creation.

## Admin Experience

- Add review filters: pending manual review, held by automation, review failed, auto-approved, auto-rejected.
- Default queue should prioritize held policy risk, held duplicate risk, failed review, then oldest pending submissions.
- Add a compact badge to each row showing `Auto approved`, `Auto rejected duplicate`, `Held: policy`, `Held: duplicate`, `Held: review failed`, or `Dry-run would approve/reject`.
- Add an expandable evidence panel with asset warnings, policy flags, nearest duplicate match, dHash distance, semantic score, confidence, model, and reviewed timestamp.
- Reuse the existing `SimilarPanel` for visual duplicate evidence where possible.
- Add a `Rerun automated review` admin action that creates a fresh review record or supersedes the previous one.

## Rollout Plan

- Phase 1: dry-run review records only for all new submissions and a backfill of current pending rows.
- Phase 2: compare dry-run decisions against manual admin outcomes and tune thresholds.
- Phase 3: enable auto-reject only for exact hash matches and dHash `0` duplicates.
- Phase 4: enable high-confidence auto-approve when there are no duplicate, policy, asset, or model warnings.
- Phase 5: consider expanding near-exact duplicate auto-reject to dHash `1..3` with corroborating metadata after reviewing false positives.

## Verification

- Run `bun test src/lib/submission-review.test.ts` for decision-matrix coverage.
- Run `bun test src/lib/security.test.ts` after adding asset-integrity regressions.
- Run `bun test` for the full Bun test suite.
- Run `bun run check` for Biome.
- Run `bun run build` for the Next app.
- Run `bun run i18n:check` only if admin copy touches translation message files.
- Run `bun scripts/review-pending-submissions.ts --dry --limit 5` against a real or mock environment to confirm records are written without status changes.
- Run `bun run dev:mock` and verify `/admin` shows review badges/evidence and existing approve/reject flows still work.
- Manually submit two identical packs and confirm dry-run would reject the duplicate while leaving status `pending`.
- Manually submit a clean unique pack and confirm dry-run would approve while leaving status `pending`.
- Manually force a model/API failure and confirm the submission remains pending with a review failure reason.

## Main Risks

- Fire-and-forget background work is not as reliable as a real queue, so the retry script is required for v1 operations.
- Policy classifiers can misread pixel art, so policy flags must hold instead of reject.
- Semantic embeddings are useful for triage but not secure enough for automatic duplicate rejection by themselves.
- Two identical submissions can arrive concurrently, so exact hash checks must compare against pending rows too and status mutation should re-check evidence immediately before applying.
- The current approval logic lives inside an API route, so extracting shared decision helpers is the first implementation slice to avoid duplicate side effects.

## Gemini Embeddings Follow-Up Plan

### Decision

- Replace OpenAI `text-embedding-3-small` embeddings with AI SDK `embed()` using model string `google/gemini-embedding-2`.
- Use the exact requested call shape where practical:
  `import { embed } from "ai"; await embed({ model: "google/gemini-embedding-2", value })`.
- Verified via AI Gateway model list that `google/gemini-embedding-2` exists and is type `embedding`.

### Key Constraint

- Existing `submitted_pets.embedding` is `vector(1536)` and existing rows were created with OpenAI embeddings.
- Gemini embeddings must not be compared with OpenAI embeddings.
- Before changing production comparison behavior, confirm the vector length returned by `google/gemini-embedding-2` through a tiny local smoke script or REPL call.
- If the dimension is not `1536`, add a new migration for the new dimension instead of editing an applied migration.

### Files To Modify

- `package.json` and `bun.lock`: add the `ai` package with Bun.
- `.env.example`: document AI Gateway credentials, likely `AI_GATEWAY_API_KEY`, and add embedding model/provider notes.
- `src/lib/query-embed.ts`: replace OpenAI embedding generation with centralized AI SDK `embed()` helper.
- `src/lib/similarity.ts`: reuse the centralized helper for pet-text embeddings.
- `src/lib/submission-review.ts`: remove the local duplicated OpenAI embedding helper and reuse the centralized helper.
- `scripts/compute-similarity.ts`: switch the backfill script to Gemini embeddings through the same helper or a script-safe equivalent.
- `src/app/api/pet-requests/route.ts`: keep using `embedQuery()` but ensure request embeddings are stored with the same Gemini/vector-dimension strategy.
- `src/lib/pet-search.ts`: keep pgvector search, but guard or update casts if the vector dimension changes.
- `src/app/api/admin/similar/[id]/route.ts`: keep semantic similarity, but guard mixed-provider vectors and update thresholds after backfill.
- `drizzle/`: add a new migration for embedding dimension/model metadata if needed; do not mutate an already-applied migration.

### Recommended Data Migration

- Add an `embedding_model` text column to `submitted_pets` and `pet_requests` so future comparisons know vector provenance.
- Add or fix `pet_requests.embedding vector(<dimension>)` because code currently writes request embeddings but migrations do not define that column.
- If Gemini returns `1536` dimensions:
  - Keep `submitted_pets.embedding vector(1536)`.
  - Backfill all rows and set `embedding_model = 'google/gemini-embedding-2'`.
- If Gemini returns a different dimension:
  - Add a new vector column or alter after clearing existing OpenAI vectors, depending on production migration safety.
  - Update every raw `::vector` cast and similarity query to the new dimension-compatible storage.

### Rollout

- Ship dependency and helper changes behind a centralized constant/env such as `PETDEX_EMBEDDING_MODEL=google/gemini-embedding-2`.
- Disable semantic duplicate auto-decisions while vectors are mixed or missing; exact hashes and dHash still work.
- Run a full backfill with `scripts/compute-similarity.ts --force` after switching the helper.
- Re-enable semantic similarity only for rows whose `embedding_model` matches the current model.
- Recalibrate thresholds for Gemini before using `0.88`/`0.95` semantic duplicate cutoffs, because OpenAI cosine score distributions may not transfer.

### Verification

- Verify local AI SDK docs/source after adding `ai`; do not rely on memory for the current `embed()` API.
- Run a one-off embedding smoke check and record the returned vector length.
- Run `bun test src/lib/submission-review.test.ts`.
- Run targeted tests/checks for embedding helpers and touched files.
- Run `bun run build`.
- Run `bun scripts/review-pending-submissions.ts --dry --limit 5` in staging after migration.
- Run `scripts/compute-similarity.ts --force` in staging and confirm `embedding_model` is set for approved and pending pets.
