From f86ae205a9a05ff4377e9c8b0814039e08f16342 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:26:54 +0800 Subject: [PATCH 01/30] feat: add image generation metadata re-stamp script --- package.json | 1 + scripts/reindex-image-generation-metadata.ts | 247 +++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 scripts/reindex-image-generation-metadata.ts diff --git a/package.json b/package.json index 987bcba5a..e44f6bc8a 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "reindex": "tsx scripts/reindex.ts", "reindex:health": "tsx scripts/reindex-health.ts", "reindex:cleanup-staged": "tsx scripts/cleanup-abandoned-reindex-generations.ts", + "images:re-stamp-generation": "tsx scripts/reindex-image-generation-metadata.ts", "supabase:recovery-status": "tsx scripts/supabase-recovery-status.ts", "promote:query-misses": "tsx scripts/promote-query-misses.ts", "promote:public-documents": "tsx scripts/promote-public-documents.ts", diff --git a/scripts/reindex-image-generation-metadata.ts b/scripts/reindex-image-generation-metadata.ts new file mode 100644 index 000000000..a537f1487 --- /dev/null +++ b/scripts/reindex-image-generation-metadata.ts @@ -0,0 +1,247 @@ +import { loadEnvConfig } from "@next/env"; +import { confirm } from "./cli-utils"; +import { committedIndexGeneration, metadataRecord } from "@/lib/reindex-pipeline"; +import { assertSupabaseHealthy, probeSupabaseHealth } from "@/lib/supabase/health"; +import { checkSupabaseProjectConfig, formatSupabaseProjectCheck } from "@/lib/supabase/project"; +import { createAdminClient } from "@/lib/supabase/admin"; +import { requireServerEnv } from "@/lib/env"; + +loadEnvConfig(process.cwd()); + +type Args = { + documentId: string | null; + ownerId: string | null; + allOwners: boolean; + limit: number; + write: boolean; + confirm: boolean; +}; + +type DocumentRow = { + id: string; + title: string | null; + file_name: string | null; + metadata: unknown; +}; + +type ImageRow = { + id: string; + index_generation_id: string | null; + metadata: unknown; +}; + +type ImagePatch = { + imageId: string; + indexGenerationId: string; + metadata: Record; +}; + +function parseArgs(argv: string[]): Args { + const args: Args = { + documentId: null, + ownerId: process.env.RAG_EVAL_OWNER_ID ?? process.env.LOCAL_NO_AUTH_OWNER_ID ?? null, + allOwners: false, + limit: 25, + write: false, + confirm: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === "--all-owners") { + args.allOwners = true; + args.ownerId = null; + continue; + } + if (token === "--write") { + args.write = true; + continue; + } + if (token === "--confirm") { + args.confirm = true; + continue; + } + + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + if (token === "--document-id" || token === "--owner-id" || token === "--limit") { + throw new Error(`Missing value for ${token}`); + } + continue; + } + if (token === "--document-id") args.documentId = value; + if (token === "--owner-id") args.ownerId = value; + if (token === "--limit") args.limit = Number.parseInt(value, 10); + if (token === "--document-id" || token === "--owner-id" || token === "--limit") { + index += 1; + } + } + + if (!Number.isFinite(args.limit) || args.limit <= 0) { + throw new Error("--limit must be a positive integer."); + } + if (!args.documentId && !args.ownerId && !args.allOwners) { + throw new Error("Provide --document-id, --owner-id, or --all-owners."); + } + return args; +} + +function rowNeedsRefresh(row: ImageRow, committedGeneration: string) { + const rowMetadataGeneration = committedIndexGeneration(row.metadata); + if (row.index_generation_id !== null && row.index_generation_id !== committedGeneration) return true; + return row.index_generation_id === null + ? rowMetadataGeneration !== committedGeneration + : rowMetadataGeneration !== committedGeneration; +} + +async function loadDocuments(args: { + supabase: ReturnType; + ownerId: string | null; + documentId: string | null; + allOwners: boolean; + limit: number; +}) { + let query = args.supabase.from("documents").select("id,title,file_name,metadata").eq("status", "indexed").order("created_at", { + ascending: false, + }); + + if (args.documentId) query = query.eq("id", args.documentId); + if (args.ownerId) query = query.eq("owner_id", args.ownerId); + if (!args.allOwners && !args.ownerId && !args.documentId) return []; + + const { data, error } = await query.limit(args.limit); + if (error) throw new Error(error.message); + return (data ?? []) as DocumentRow[]; +} + +async function loadImages(args: { + supabase: ReturnType; + documentId: string; +}) { + const rows: ImageRow[] = []; + for (let offset = 0; ; offset += 1000) { + const { data, error } = await args.supabase + .from("document_images") + .select("id,index_generation_id,metadata") + .eq("document_id", args.documentId) + .range(offset, offset + 999); + if (error) throw new Error(error.message); + const page = (data ?? []) as ImageRow[]; + rows.push(...page); + if (page.length < 1000) break; + } + return rows; +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + requireServerEnv(); + + const projectCheck = checkSupabaseProjectConfig( + { + NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, + SUPABASE_PROJECT_REF: process.env.SUPABASE_PROJECT_REF, + SUPABASE_PROJECT_NAME: process.env.SUPABASE_PROJECT_NAME, + }, + { requireMetadata: true }, + ); + if (projectCheck.status === "mismatch") { + throw new Error(`Supabase project mismatch: ${formatSupabaseProjectCheck(projectCheck)}`); + } + if (projectCheck.warnings.length > 0) { + console.log(`Supabase project warning: ${projectCheck.warnings.join(" ; ")}`); + } + + const supabase = createAdminClient(); + console.log(`Target project: ${projectCheck.expected.name} (${projectCheck.expected.ref})`); + assertSupabaseHealthy(await probeSupabaseHealth(supabase), "Re-stamp document image generation"); + + const documents = await loadDocuments({ + supabase, + ownerId: args.ownerId, + documentId: args.documentId, + allOwners: args.allOwners, + limit: args.limit, + }); + + if (documents.length === 0) { + console.log("No indexed documents matched the target filter."); + return; + } + + const label = args.allOwners ? "all owners" : args.documentId ? `document ${args.documentId}` : `owner ${args.ownerId}`; + console.log(`Loaded ${documents.length} indexed document(s) for ${label} inspection.`); + + const patches: ImagePatch[] = []; + let inspectedImageCount = 0; + let documentsWithCandidates = 0; + + for (const document of documents) { + const committedGeneration = committedIndexGeneration(document.metadata); + if (!committedGeneration) { + console.log(`SKIP ${document.title ?? document.file_name ?? document.id}: missing index_generation_id in documents.metadata`); + continue; + } + + const images = await loadImages({ supabase, documentId: document.id }); + const staleRows = images.filter((image) => rowNeedsRefresh(image, committedGeneration)); + inspectedImageCount += images.length; + + if (staleRows.length === 0) { + console.log(`OK ${document.title ?? document.file_name ?? document.id}: ${images.length} image rows aligned`); + continue; + } + + documentsWithCandidates += 1; + console.log( + `MISMATCH ${document.title ?? document.file_name ?? document.id}: ${staleRows.length}/${images.length} image rows need re-stamp`, + ); + for (const row of staleRows) { + patches.push({ + imageId: row.id, + indexGenerationId: committedGeneration, + metadata: { ...metadataRecord(row.metadata), index_generation_id: committedGeneration }, + }); + } + } + + if (patches.length === 0) { + console.log(`\nNo stale image generation metadata found. Images inspected: ${inspectedImageCount}.`); + return; + } + + console.log(`\nStale rows found: ${patches.length} image rows across ${documentsWithCandidates} documents.`); + if (!args.write) { + console.log("Dry run complete. Run with --write --confirm to apply changes."); + return; + } + + const approved = args.confirm || (await confirm(`Apply re-stamp to ${patches.length} image rows?`)); + if (!approved) { + console.log("Operation cancelled. No rows updated."); + return; + } + + for (let start = 0; start < patches.length; start += 8) { + const batch = patches.slice(start, start + 8); + await Promise.all( + batch.map(async (patch) => { + const { error } = await supabase + .from("document_images") + .update({ + index_generation_id: patch.indexGenerationId, + metadata: patch.metadata, + }) + .eq("id", patch.imageId); + if (error) throw new Error(`Failed to re-stamp image row ${patch.imageId}: ${error.message}`); + }), + ); + } + + console.log(`Completed re-stamp. Updated ${patches.length} image rows across ${documentsWithCandidates} documents.`); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}); From bac17526dd674d4e5451949d92000b51e768b4d0 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:31:00 +0800 Subject: [PATCH 02/30] chore: reconcile ingestion RPC execute privileges, schema.sql and drift manifest --- .cursor/skills/accessibility-review/SKILL.md | 18 ++ .../skills/ai-architecture-review/SKILL.md | 23 ++ .cursor/skills/api-review/SKILL.md | 28 ++ .cursor/skills/code-quality-review/SKILL.md | 19 ++ .cursor/skills/design-review/SKILL.md | 18 ++ .../frontend-architecture-review/SKILL.md | 18 ++ .cursor/skills/performance-review/SKILL.md | 18 ++ .../skills/release-readiness-review/SKILL.md | 20 ++ .cursor/skills/repo-auditor/SKILL.md | 15 + .cursor/skills/security-review/SKILL.md | 20 ++ .../CHANGELOG.md | 29 ++ .../supabase-postgres-best-practices/SKILL.md | 22 +- .../references/_contributing.md | 28 +- .../references/_sections.md | 8 - .../references/security-rls-performance.md | 12 +- .cursor/skills/supabase/CHANGELOG.md | 35 +++ .cursor/skills/supabase/SKILL.md | 45 ++- .cursor/skills/testing-review/SKILL.md | 19 ++ .cursor/skills/ux-review/SKILL.md | 18 ++ package.json | 3 + scripts/compare-retrieval-eval.ts | 87 ++++++ scripts/embed-registry-records.ts | 162 ++++++++++ scripts/eval-retrieval.ts | 30 +- scripts/reindex-image-generation-metadata.ts | 51 +++- scripts/seed-differential-records.ts | 12 + scripts/seed-medication-records.ts | 12 + scripts/seed-registry-records.ts | 14 + src/lib/differential-seed.ts | 11 +- src/lib/differentials.ts | 4 +- src/lib/document-index-units.ts | 78 ++++- src/lib/env.ts | 4 + src/lib/forms.ts | 2 +- src/lib/medication-seed.ts | 11 +- src/lib/rag.ts | 96 ++++-- src/lib/registry-corpus.ts | 277 ++++++++++++++++++ src/lib/registry-seed.ts | 11 +- src/lib/source-governance.ts | 17 +- src/lib/source-metadata.ts | 2 + src/lib/types.ts | 4 +- supabase/drift-allowlist.json | 23 +- supabase/drift-manifest.json | 14 +- ...000_reconcile_ingestion_rpc_privileges.sql | 12 + supabase/schema.sql | 21 ++ tests/document-index-units.test.ts | 35 +++ tests/registry-corpus.test.ts | 70 +++++ tests/source-governance.test.ts | 38 +++ 46 files changed, 1391 insertions(+), 123 deletions(-) create mode 100644 .cursor/skills/accessibility-review/SKILL.md create mode 100644 .cursor/skills/ai-architecture-review/SKILL.md create mode 100644 .cursor/skills/api-review/SKILL.md create mode 100644 .cursor/skills/code-quality-review/SKILL.md create mode 100644 .cursor/skills/design-review/SKILL.md create mode 100644 .cursor/skills/frontend-architecture-review/SKILL.md create mode 100644 .cursor/skills/performance-review/SKILL.md create mode 100644 .cursor/skills/release-readiness-review/SKILL.md create mode 100644 .cursor/skills/repo-auditor/SKILL.md create mode 100644 .cursor/skills/security-review/SKILL.md create mode 100644 .cursor/skills/supabase-postgres-best-practices/CHANGELOG.md create mode 100644 .cursor/skills/supabase/CHANGELOG.md create mode 100644 .cursor/skills/testing-review/SKILL.md create mode 100644 .cursor/skills/ux-review/SKILL.md create mode 100644 scripts/compare-retrieval-eval.ts create mode 100644 scripts/embed-registry-records.ts create mode 100644 src/lib/registry-corpus.ts create mode 100644 supabase/migrations/20260709150000_reconcile_ingestion_rpc_privileges.sql create mode 100644 tests/registry-corpus.test.ts diff --git a/.cursor/skills/accessibility-review/SKILL.md b/.cursor/skills/accessibility-review/SKILL.md new file mode 100644 index 000000000..209a598a9 --- /dev/null +++ b/.cursor/skills/accessibility-review/SKILL.md @@ -0,0 +1,18 @@ +--- +name: accessibility-review +description: Reviews keyboard navigation support, semantic HTML tags, drawer/dialog behaviors, focus management, and screen-reader standards. Use during UI audits. +--- + +# Accessibility Review Skill + +Use this skill when reviewing user interfaces to ensure compliance with a11y standards. + +## Review Checklist + +### 1. Keyboard & Focus Control +- **Interactive Elements:** Verify all button-like and link-like components use semantic HTML elements (` {imageCount > 0 && ( - + {imageCount} indexed image{imageCount === 1 ? "" : "s"} )} @@ -124,7 +124,7 @@ export function SourcePassageLinks({ href={sourceResultHref(source)} className={cn( compact ? metadataPill : floatingControl, - "min-h-11 gap-1.5 px-2.5 text-2xs sm:min-h-9 sm:px-3", + "min-h-tap gap-1.5 px-2.5 text-2xs sm:min-h-9 sm:px-3", )} title={`${source.title} · page ${source.page_number ?? "n/a"} · chunk ${source.chunk_index}`} aria-label={`Open source passage #${index + 1}`} diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index e899fe11e..5c1b30adf 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -559,7 +559,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) { diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index 9fc8159da..802b5dde3 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -75,7 +75,7 @@ export const chatActionRow = export const chatMicroAction = "inline-flex min-h-tap min-w-tap items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold text-[color:var(--text-muted)] transition hover:bg-[color:var(--clinical-accent-soft)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-50"; export const sourceCapsule = - "source-capsule-hover focus-ring-premium inline-flex min-h-tap items-center gap-1 rounded-md border border-[color:var(--border)] bg-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] px-2 text-[11px] font-medium text-[color:var(--clinical-accent)] transition hover:border-[color:var(--clinical-accent-border)] sm:px-2.5"; + "source-capsule-hover focus-ring-premium inline-flex min-h-tap items-center gap-1 rounded-md border border-[color:var(--border)] bg-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] px-2 text-2xs font-medium text-[color:var(--clinical-accent)] transition hover:border-[color:var(--clinical-accent-border)] sm:px-2.5"; export const sourceCapsuleCountBadge = "nums inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full bg-[color:var(--surface-raised)] px-1.5 text-2xs font-semibold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]"; export const evidenceRow = diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index ae7e65f53..7d39c5d85 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-09T08:52:11.900Z", + "generated_at": "2026-07-09T11:02:02.346Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", "schema_sha256": "3a60bf1561142aed58e45a1cf8a9605ccc715dd7358e154c4ad9879b82be4cd5", - "replay_seconds": 16, + "replay_seconds": 28, "snapshot": { "views": [ { From c97f79f419534c46529d3596d7a73e31e9a83aec Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:15:14 +0800 Subject: [PATCH 27/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/lib/rag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 333cc5db5..229c6be9e 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -1111,7 +1111,7 @@ function addOpenAIUsage(total: OpenAITokenUsage, usage?: OpenAITokenUsage) { }; } -/** Has open aiusage. */ +/** Has OpenAI usage. */ function hasOpenAIUsage(usage: OpenAITokenUsage) { return Object.values(usage).some((value) => typeof value === "number" && value > 0); } From a7d1d2d8956800518c13e84389dabc5b36b0f9d9 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:15:19 +0800 Subject: [PATCH 28/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/lib/rag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 229c6be9e..2092186b5 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -1099,7 +1099,7 @@ function safeFallbackAnswer(raw: string, results: SearchResult[], query?: string return applyNumericVerification(answer); } -/** Add open aiusage. */ +/** Add OpenAI usage. */ function addOpenAIUsage(total: OpenAITokenUsage, usage?: OpenAITokenUsage) { if (!usage) return total; return { From 495816869ac37b281b5140e902d4f44065123f06 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 9 Jul 2026 21:15:28 +0800 Subject: [PATCH 29/30] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/lib/rag-routing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/rag-routing.ts b/src/lib/rag-routing.ts index 2f4a2ca14..e52422e6f 100644 --- a/src/lib/rag-routing.ts +++ b/src/lib/rag-routing.ts @@ -340,7 +340,7 @@ const explicitWithholdActionPattern = /** Snippet has co occurring blood withhold evidence. */ function snippetHasCoOccurringBloodWithholdEvidence(snippet: string) { const sentences = snippet - .split(/(?<=[.!?])\s+/) + .split(/(?<=[.!?])\s+|\r?\n+/) .map((sentence) => sentence.trim()) .filter(Boolean); const candidates = sentences.length > 0 ? sentences : [snippet]; From 6186b89fdb7b5f4fe2fe56157c00aae5e156fec6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:18:31 +0000 Subject: [PATCH 30/30] fix: format docs/branch-review-ledger.md to pass Prettier check --- docs/branch-review-ledger.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 4c06bd362..bd05d445a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -18,9 +18,9 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD ## Review Records -| Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | -| ---------- | -------------- | --------------------------------------- | -------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| 2026-07-09 | example/branch | abc1234567890abcdef1234567890abcdef1234 | branch-cleanup | Example: already merged into `main`; no unique patch content. | `git log --right-only --cherry-pick main...example/branch`; `git diff --name-status main...example/branch` | -| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | design-review | Passed; resolved 3 minor P3 design system alignment issues (fixed arbitrary font sizes to `text-2xs` and `min-h-11` to `min-h-tap`). | `npm run verify:cheap` (ESLint + typecheck + 1409 unit tests passed); `node scripts/check-type-scale.mjs`; z-index & compile checks | -| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | ai-architecture-review | AI architecture review completed. Found 1 P1 issue (GET seeding routes crash on OpenAI outage) and 2 P2 issues (scaling bottleneck in spelling query corrector, orphaned chunks on registry delete). All 1409 tests passed. | `git diff origin/main...HEAD`, `npm run verify:cheap` (ESLint + typecheck + 1409 unit tests passed) | -| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | api-review | No high-confidence P0/P1/P2 issues. Identified minor validation sequence and parameter bounds checks (P3). | `npm run verify:cheap` (TypeScript typecheck, ESLint, and isolated Vitest runs on medications-route, rag-answer-fallback, and rag-cache-invalidation) | +| Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | +| ---------- | -------------------------- | ---------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-09 | example/branch | abc1234567890abcdef1234567890abcdef1234 | branch-cleanup | Example: already merged into `main`; no unique patch content. | `git log --right-only --cherry-pick main...example/branch`; `git diff --name-status main...example/branch` | +| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | design-review | Passed; resolved 3 minor P3 design system alignment issues (fixed arbitrary font sizes to `text-2xs` and `min-h-11` to `min-h-tap`). | `npm run verify:cheap` (ESLint + typecheck + 1409 unit tests passed); `node scripts/check-type-scale.mjs`; z-index & compile checks | +| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | ai-architecture-review | AI architecture review completed. Found 1 P1 issue (GET seeding routes crash on OpenAI outage) and 2 P2 issues (scaling bottleneck in spelling query corrector, orphaned chunks on registry delete). All 1409 tests passed. | `git diff origin/main...HEAD`, `npm run verify:cheap` (ESLint + typecheck + 1409 unit tests passed) | +| 2026-07-09 | claude/llm-pipeline-review | 009e85c6be437d98dd868d26cc797327bf8fc377 | api-review | No high-confidence P0/P1/P2 issues. Identified minor validation sequence and parameter bounds checks (P3). | `npm run verify:cheap` (TypeScript typecheck, ESLint, and isolated Vitest runs on medications-route, rag-answer-fallback, and rag-cache-invalidation) |