Harden clinical search and app workflows - #87
Conversation
Add atomic reindex generation safeguards, RAG answer cleanup, application/mockup route updates, verification tooling, and focused browser stability fixes.
Refine WA health site and program mappings used by document organization.
There was a problem hiding this comment.
Pull request overview
This PR hardens the clinical indexing/reindexing pipeline by introducing an atomic “commit generation” step (so retrieval/enrichment only consume committed artifacts), while also improving RAG answer/display quality and modernizing app navigation/workflows with updated UI and verification tooling.
Changes:
- Introduces an atomic index-generation commit flow (RPC + worker/app filtering by committed generation) and improves reindex safety checks.
- Improves RAG answer text sanitization/polish and related trust/fallback behaviors, with expanded unit coverage.
- Updates application routing/launchers and sheet/dialog UX, plus adds CI environment validation and process documentation updates.
Reviewed changes
Copilot reviewed 111 out of 115 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| worker/table-facts.ts | Formatting-only refactor of helper signature. |
| worker/main.ts | Adds commit-generation RPC usage + propagates index_generation_id into artifacts; updates atomic reindex behavior. |
| tests/worker-visual-capture.test.ts | Updates assertions to match new worker vector-write and agent-repair behavior. |
| tests/visual-intelligence.test.ts | Formatting-only change. |
| tests/ui-tools.spec.ts | Replaces /tools coverage with /applications launcher coverage and link expectations. |
| tests/ui-stress.spec.ts | Stabilizes routes/selectors and daily-actions opening for stress flows. |
| tests/ui-accessibility.spec.ts | Stabilizes setup-status routing and dashboard assertions/selectors. |
| tests/supabase-schema.test.ts | Adds coverage for atomic commit RPC + committed-generation filters and grants. |
| tests/retrieval-query-variants.test.ts | Formatting-only change. |
| tests/reindex-pipeline.test.ts | Adds tests for committed-generation helpers and atomic reindex candidacy. |
| tests/rag-trust.test.ts | Adds test for stripping footnote markers and normalizing headings. |
| tests/rag-cache-invalidation.test.ts | Adds explicit timeout to reduce flakiness. |
| tests/rag-answer-text.test.ts | Adds coverage for new answer-prose cleanup and quality heuristics. |
| tests/rag-answer-fallback.test.ts | Updates fallback expectations (evidence-gap behavior, sections handling, telemetry arrays). |
| tests/private-client-auth.test.ts | Adds tests for detecting placeholder/unusable browser Supabase keys. |
| tests/private-access-routes.test.ts | Adds tests for committed-generation filtering and safety checks during reindex/listing. |
| tests/ingestion-quality-route.test.ts | Formatting-only change. |
| tests/indexing-v3-agent.test.ts | Reads edge function source via fs once; removes async per-test read. |
| tests/embedding-dimensions.test.ts | Formatting-only change. |
| tests/document-organization.test.ts | Adds coverage for new document-type classification patterns and review-status logic. |
| tests/document-index-units.test.ts | Formatting-only change. |
| tests/document-enrichment.test.ts | Extends Supabase mock query builder with .is() to match usage. |
| tests/display-text.test.ts | Adds test ensuring cached answer display text is polished before render. |
| tests/deep-memory.test.ts | Removes trailing whitespace line. |
| tests/app-modes.test.ts | Adds contract tests for new app-mode routing/search behavior. |
| supabase/schema.sql | Adds commit-generation RPC, committed-generation helpers, generation filtering, unique generation index, and indexing-v3 invoker function + grants. |
| supabase/migrations/20260628000000_atomic_reindex_generation_commit.sql | New migration implementing atomic commit + patches retrieval functions to filter committed generations. |
| supabase/functions/indexing-v3-agent/behavior.ts | Formatting/typing normalization; no functional change intended. |
| src/lib/visual-intelligence.ts | Minor formatting/parentheses adjustments; small readability tweaks. |
| src/lib/types.ts | Adds document organization types; formatting for unions. |
| src/lib/tools.ts | Switches Medications tool to internal dashboard route and internal target. |
| src/lib/supabase/health.ts | Formatting for union type. |
| src/lib/supabase/client.tsx | Adds isUsableBrowserSupabaseKey guard against placeholder keys; trims publishable key. |
| src/lib/source-governance.ts | Updates refusal copy to be more actionable and less misleading. |
| src/lib/search-scope.ts | Formatting-only changes for query-building lines. |
| src/lib/reindex-pipeline.ts | Adds committed-generation helpers + atomic reindex candidate detection. |
| src/lib/rag-answer-text.ts | Adds comprehensive prose polishing + “quality issue” detection helpers; integrates polish into sanitization. |
| src/lib/public-rate-limit.ts | Adds in-memory eviction of expired buckets to prevent leak. |
| src/lib/openai.ts | Formatting for union type. |
| src/lib/index-quality.ts | Minor readability formatting; no logic change intended. |
| src/lib/image-filtering.ts | Formatting for union type. |
| src/lib/document-organization.ts | Expands site/type classification patterns and refines review-status logic. |
| src/lib/document-index-units.ts | Propagates index_generation_id into index-unit metadata; formatting/refactors for readability. |
| src/lib/document-enrichment.ts | Updates label inference to align with expanded document type taxonomy; removes generic “clinical guideline” label. |
| src/lib/deep-memory.ts | Propagates/filters memory cards by committed generation and writes generation into derived artifacts. |
| src/lib/clinical-vocabulary.ts | Formatting for union type. |
| src/lib/clinical-search.ts | Formatting-only changes around conditionals and derived strings. |
| src/lib/clinical-safety.ts | Sanitizes finding text via sanitizeAnswerText to reduce artefacts in surfaced safety findings. |
| src/lib/app-modes.ts | New centralized app-mode/search routing config for dashboard and mockups. |
| src/lib/api-rate-limit.ts | Adds in-memory eviction and stores reset timestamp to prevent leak. |
| src/lib/answer-ranking.ts | Reduces noisy query highlighting; narrows and devalues low-signal highlight terms. |
| src/components/user-home-profile/index.ts | Removes deprecated profile mockup export. |
| src/components/ui/sheet.tsx | Adds portal support and richer header slots; pointer event handling; focus-return support. |
| src/components/settings-search-mockups/index.ts | New barrel export for settings search mockups. |
| src/components/DocumentViewer.tsx | Formatting-only change. |
| src/components/DashboardFloatingFab.tsx | Updates FAB link from Tools to Applications route. |
| src/components/clinical-dashboard/global-mockup-search-shell.tsx | Adds shared header/search shell for mockup routes driven by app modes. |
| src/components/clinical-dashboard/document-ui.tsx | Adjusts badge styling for consistency with updated theme tokens. |
| src/components/clinical-dashboard/display-text.ts | Applies answer-prose polish to cached display text. |
| src/components/clinical-dashboard/dashboard-shell.tsx | Improves mobile sheet behavior and focus restoration; passes new Sheet header props. |
| src/app/page.tsx | Parses mode/q/focus to seed initial dashboard state. |
| src/app/mockups/user-home-profile/page.tsx | Removes obsolete profile mockup route. |
| src/app/mockups/settings-search-privacy/page.tsx | New runnable mockup route using shared mockup shell. |
| src/app/mockups/settings-search-general/page.tsx | New runnable mockup route using shared mockup shell. |
| src/app/mockups/settings-search-clinical/page.tsx | New runnable mockup route using shared mockup shell. |
| src/app/mockups/recent-searches-bottom/page.tsx | Adds runnable mockup page for bottom “recent searches” concept. |
| src/app/mockups/mode-dropdown/page.tsx | Removes profile option from mock mode list. |
| src/app/mockups/layout.tsx | New shared mockups layout wrapping pages in global mockup shell. |
| src/app/medications/page.tsx | Redirects legacy /medications entry to prescribing mode. |
| src/app/medications/[slug]/page.tsx | Adds medication deep link route (currently only acamprosate). |
| src/app/home/page.tsx | Removes obsolete /home profile route. |
| src/app/applications/page.tsx | Adds Applications launcher page route. |
| src/app/api/ingestion/quality/route.ts | Mostly formatting; clarifies some detail strings and sorts. |
| src/app/api/documents/route.ts | Removes demo-data fallback from “missing server env” error handling. |
| src/app/api/documents/bulk/reindex/route.ts | Filters enrichment inputs to committed generation; always checks active jobs; atomic reindex update behavior. |
| src/app/api/documents/[id]/reindex/route.ts | Same as bulk: committed-generation filtering; always checks active jobs; atomic reindex update behavior. |
| scripts/retrieval-health.ts | Refactors Promise.all destructuring + minor formatting. |
| scripts/reindex.ts | Refactors imports and Promise.all formatting; no functional change intended. |
| scripts/profile-retrieval-rpcs.ts | Formatting/clarity improvements in arg parsing and output path logic. |
| scripts/playwright-base-url.ts | Adds retries/timeouts to local project identity verification. |
| scripts/eval-retrieval.ts | Formatting and small control-flow clarity tweaks. |
| scripts/enrich-documents.ts | Formatting-only change. |
| scripts/check-runtime.ts | Formatting-only change (multi-line signature). |
| scripts/check-edge-functions.mjs | Formatting-only change. |
| scripts/check-ci-env.mjs | New script validating required public env and optional E2E auth env in CI/local runs. |
| scripts/backfill-enrichment.ts | Formatting-only changes. |
| package.json | Updates npm packageManager + Prettier version; adds ci:env-check script. |
| package-lock.json | Lockfile update for Prettier version bump. |
| mockups/user-home-profile/page.tsx | Removes obsolete forwarding mockup entry. |
| mockups/README.md | Updates mockups routing notes and documents shared mockup shell usage. |
| eslint.config.mjs | Adds ignore patterns for broken/stale node_modules cache folders. |
| docs/reindex-runbook.md | Fixes markdown table formatting for readability. |
| docs/process-hardening.md | Updates references from /tools to /applications. |
| docs/clinical-chat-ui-implementation-plan.md | Fixes markdown tables formatting for readability. |
| docs/branch-cleanup-guide.md | Adds branch cleanup runbook doc. |
| AGENTS.md | Adds whitespace separators after embedded instruction blocks (formatting). |
| .gitignore | Ignores additional local agent/playwright artifacts and auth/cache directories. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Uh oh!
There was an error while loading. Please reload this page.
Use the shared document organization profile helper for document drawer filters.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Stabilize local project identity mocking in UI smoke tests and broaden generated document classification labels.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:98c4caeaea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (atomicReindex) { | ||
| await updateDocument(job.document_id, { error_message: null }); |
There was a problem hiding this comment.
Filter document-scoped search by committed generation
When an already indexed document is reindexed, this keeps documents.status = 'indexed' while the worker inserts new document_chunks for a fresh index_generation_id before the commit step. The global match RPCs are generation-filtered, but the document viewer search path still calls search_document_chunks (src/app/api/documents/[id]/search/route.ts), whose SQL only checks d.status = 'indexed'; during an in-progress or failed atomic reindex, searching within that document can therefore return partial, uncommitted chunks. Add the committed-generation predicate to that RPC/fallback path, or otherwise keep this document out of indexed search until the generation is committed.
Useful? React with 👍 / 👎.
| metadata: sanitizeJsonbRecord({ | ||
| ...(image.metadata ?? {}), | ||
| extractor: "local-worker", | ||
| index_generation_id: indexGenerationId, |
There was a problem hiding this comment.
Use generation-specific paths for atomic images
For atomic reindexes of an already indexed document, the worker uploads new image bytes before the generation is committed, but the storage path still resolves to the stable .../images/<documentId>/image-N name and the upload uses upsert: true just above. That overwrites the bytes referenced by the old committed document_images rows, so while the reindex is still running—or if it later fails—existing image citations can serve new/partial image content under old metadata. Include the generation id in the storage path or defer overwriting committed paths until commit succeeds.
Useful? React with 👍 / 👎.
| /\b(?:Lithicarb|Quilonum\s+SR|Campral)[®™]?|\b(?:imprest|formulary)\s+(?:location|one)\b.*?(?=\b(?:therapy|treatment|start|commence|begin|check|monitor|baseline|dose|dosing)\b|[.?!]|$)/gi; | ||
| const imprestLocationPattern = | ||
| /\bimprest\s+location\s*:\s*.*?(?=\b(?:therapy|treatment|start|commence|begin|check|monitor|baseline|dose|dosing)\b|$)/gi; | ||
| const allCapsSourceHeadingPattern = /\b(?=[A-Z0-9/&,+() -]*\s[A-Z0-9])[A-Z][A-Z0-9/&,+() -]{8,}\b/g; |
There was a problem hiding this comment.
Preserve uppercase clinical abbreviations in answers
For monitoring answers that legitimately list multiple uppercase tests, such as “FBC, UEC, LFT and ECG,” this pattern matches the lab list as an all-caps source heading. Because polishClinicalAnswerProse removes matches and hasClinicalAnswerQualityIssue rejects them, common source-backed monitoring answers can lose the actual lab names or be downgraded to an unsupported evidence gap. Narrow this to real source headings/form codes or whitelist clinical abbreviations.
Useful? React with 👍 / 👎.
| id: "favourites", | ||
| label: "Favourites", | ||
| description: "Saved sources and workflows", | ||
| devOnly: true, |
There was a problem hiding this comment.
Hide dev-only favourites from production launcher
The applications launcher exposes a Favourites app with href: "/?mode=favourites", but this mode is marked devOnly. In production, src/app/page.tsx accepts only isAppModeVisible modes and falls back to "answer", so clicking Launch Favourites on /applications opens the Answer workspace instead of the saved-items workflow. Either hide that launcher entry outside development or make the linked mode visible/routable.
Useful? React with 👍 / 👎.
| targetResults.map((result) => [result.document_id, committedIndexGeneration(result.source_metadata)] as const), | ||
| ); | ||
| for (const chunk of data) { | ||
| const committedGeneration = committedGenerationByDocument.get(chunk.document_id); | ||
| if (chunk.index_generation_id && chunk.index_generation_id !== committedGeneration) continue; |
There was a problem hiding this comment.
Keep generation id when packing adjacent context
After an atomic reindex commits, the new document_chunks rows have a non-null index_generation_id, but answer generation ranks normalizeSearchResults(search.results) before calling this context packer, and normalizeSourceMetadata drops index_generation_id from source_metadata. This map therefore records null, so the check below skips every adjacent row for committed new-generation chunks and generated answers lose the surrounding source context for reindexed documents. Carry the raw document generation (or the chunk generation) into this filter instead of reading it from normalized source_metadata.
Useful? React with 👍 / 👎.
…etry-main # Conflicts: # src/components/clinical-dashboard/document-search-results.tsx # tests/indexing-v3-agent.test.ts # tests/worker-visual-capture.test.ts
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:1c795b871e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| chunks: chunks ?? [], | ||
| images: committedRows(document, images ?? []).map(withImageTableMetadata), | ||
| tableFacts: committedRows(document, tableFactsResult.data ?? []), | ||
| chunks: committedRows(document, chunks ?? []), |
There was a problem hiding this comment.
Filter chunks before paginating the viewer
During an atomic reindex of an indexed document, old committed chunks and new-generation chunks share chunk_index until the commit step. This endpoint applies .range() before committedRows, so a window such as offset 0/limit 16 can fetch old+new pairs for indexes 0-7 and then filter down to only 8 committed chunks, making the document viewer omit committed chunks even though the old index should remain stable. Add the generation predicate to the query before ranging, or over-fetch and paginate after filtering.
Useful? React with 👍 / 👎.
| const committedGenerationByDocument = new Map( | ||
| results.map((result) => [result.document_id, committedIndexGeneration(result.source_metadata)] as const), | ||
| ); |
There was a problem hiding this comment.
Use raw generation metadata when attaching images
When a result comes from document-lookup or memory-card candidate paths, source_metadata has already been normalized and no longer contains index_generation_id. For newly indexed/reindexed documents the image rows do carry metadata.index_generation_id, so this map supplies null and the filter below drops all current page/direct images; image, table, and flowchart answers in those paths lose their visual evidence. Carry the raw document generation alongside the result instead of deriving it from normalized source metadata.
Useful? React with 👍 / 👎.
| delete from public.document_chunks | ||
| where document_id = p_document_id | ||
| and (index_generation_id is null or index_generation_id <> p_index_generation_id); |
There was a problem hiding this comment.
Clear stale generated summaries at generation commit
When atomic reindex commits the core chunks but enrichment is deferred or later fails, this cleanup removes generation-scoped artifacts but leaves prior generated document_summaries and document_labels untouched. The match RPCs still attach document_summary_text and document_label_metadata, so the newly committed chunks can be ranked or presented with stale generated metadata from the previous generation until enrichment reruns. Clear or generation-tag generated summaries/labels during the commit while preserving manual labels.
Useful? React with 👍 / 👎.
| where document_id = p_document_id | ||
| and (index_generation_id is null or index_generation_id <> p_index_generation_id); | ||
| delete from public.document_images |
There was a problem hiding this comment.
Remove superseded image objects at generation commit
Now that worker uploads images under generation-specific paths, deleting superseded document_images rows here drops the only recorded storage paths without removing or enqueuing the old objects. A successful atomic reindex, or a successful retry after a failed generation, leaves prior-generation image bytes in clinical-images indefinitely because document-delete cleanup only reads rows that still exist. Capture these paths into storage_cleanup_jobs or remove them before deleting the rows.
Useful? React with 👍 / 👎.
| indexing_v3_agent_updated_at: indexedAt, | ||
| ...metrics, | ||
| }; | ||
| await commitDocumentIndexGeneration({ |
There was a problem hiding this comment.
Preserve committed generation during inline enrichment
When WORKER_INLINE_ENRICHMENT=true, committing here sets documents.metadata.index_generation_id, but the following inline upsertDocumentEnrichment({ document: job.documents }) rewrites documents.metadata from the stale pre-reindex row before the final update restores it. During that window, and permanently if the worker is killed before the final metadata update, the generation-filtered RPCs see no committed generation and hide the freshly committed chunks. Pass committedCoreMetadata or freshly loaded document metadata into enrichment, or stop that helper from overwriting the generation.
Useful? React with 👍 / 👎.
Surfaced during brace-expansion triage (PR #1314). npm audit reports 16 high advisories that all cascade from one root cause; npm's only offered remedy is eslint@10.8.0 (isSemVerMajor). All paths are dev-scoped and absent from the production bundle, so this is cleanup rather than exposure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This session ran `npm run format` in a container with no node_modules. Prettier resolved through npx as 3.8.1 while the repo pins ^3.9.6 (lockfile 3.9.6), and the older binary rewrote 31 files nobody had touched — including src/lib/rag/rag-cache.ts, src/lib/rag/rag-provider.ts and src/lib/openai.ts, which would have flipped a docs-only PR into ragRanking/clinicalRisk scope under classifyPullRequestFiles. It was an artifact, not drift: `npx prettier@3.9.6 --check` on the same files reports them clean. The reformat was reverted and never committed; this row records the trap so a later uninstalled session does not repeat it, and proposes pinning the binary in format/format:changed or failing closed on a lockfile mismatch. Same failure class as archived row #87 (knip findings from an uninstalled worktree), but worse: knip only reports, format writes, so the false result arrives already applied to the working tree. Filed as a pending request rather than folded into the preceding reconcile: a reconcile must only consume requests already present on its base, and check:ledger-write-discipline rejects the bundle otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0122UC4BL5R39kz41WbBYcPj
Summary
Verification
npm run verify:cheap(final rerun clean: runtime, lint, typecheck, 537 Vitest tests passed)npm run verify:ui(35 Chromium tests passed before final test-only follow-ups)npm run verify:releasebefore release or handoff confidence claims (runtime, lint, typecheck, Vitest, and build passed; full command hit stale dev-server/browser failures afternext buildrewrote.next; restarted only the repo-owned dev server and rerannpm run test:e2e, 102/102 passed)npx playwright test tests/ui-smoke.spec.ts --project=chromium --grep "private mode unauthenticated|desktop daily actions close|demo answer flow reaches" --reporter=line(3/3 passed)npm run format:check(global check still reports older baseline formatting issues; focused Prettier check for changed files passed, andgit diff --checkpassed)npm run check:production-readinesswhen clinical workflow, privacy, environment, Supabase, source governance, or deployment behavior changed (ready; local.env.localservice-role marker warning only)npm run check:supabase-projectnpm install/ npm audit (548 packages audited, 0 vulnerabilities)Clinical Governance Preflight
Complete this section when the change touches ingestion, answer generation, search/ranking, source rendering, document access, privacy, production env, or clinical output.
Clinical KB Database(sjrfecxgysukkwxsowpy)Notes
.codex/,.agents/,.playwright-cli/,playwright-cli/,playwright/.auth/, andskills-lock.json.verify:releasecommand can destabilize an already-running dev server becausenext buildrewrites.next; fresh-servernpm run test:e2epassed after the build.