diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 835cc483..71e00c3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,15 @@ jobs: - name: PR policy self-test run: npm run check:pr-policy + - name: Codebase index coverage + run: npm run docs:check-index + + - name: Dependency and duplicate-export hygiene + run: npm run check:knip + + - name: Maintainability hotspot budgets + run: npm run check:maintainability-budgets + - name: Format check run: npm run format:check diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 5c709604..3df40e36 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -56,11 +56,15 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map | `/differentials`, `/diagnoses`, `/presentations` | `src/app/differentials/` | | `/dsm`, `/dsm/search`, `/dsm/compare`, `/dsm/diagnoses/[slug]` | `src/app/dsm/` | | `/documents/search`, `/source`, `/evidence`, `/[id]` | `src/app/documents/` | +| `/factsheets`, `/factsheets/search`, `/factsheets/[slug]` | `src/app/factsheets/` | | `/favourites` | `src/app/favourites/page.tsx` | | `/forms`, `/forms/[slug]` | `src/app/forms/` | | `/medications`, `/medications/[slug]` | `src/app/medications/` | +| `/privacy` | `src/app/privacy/page.tsx` | +| `/reference/colour-coding` | `src/app/reference/` | | `/services`, `/services/[slug]` | `src/app/services/` | | `/therapy-compass` | `src/app/therapy-compass/` | +| `/tools` | `src/app/tools/` | | `/specifiers`, `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, `/specifiers/map` | `src/app/specifiers/` | | `/formulation`, `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, `/formulation/map` | `src/app/formulation/` | | `/mockups/*` | `src/app/mockups/` (404 in production) | @@ -70,6 +74,7 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map | Area | Routes | Entry files | | ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| Account | `/api/account/favourites`, `/api/account/preferences` | `account/` | | Answers | `/api/answer`, `/api/answer/stream`, `/api/answer-feedback` | `answer/route.ts`, `answer/stream/route.ts`, `answer-feedback/` | | Search | `/api/search`, `/api/search/interaction`, `/api/search/universal` | `search/` | | Upload | `/api/upload` | `upload/route.ts` | @@ -158,7 +163,7 @@ Structured map for AI agents and onboarding. For live routes, see `docs/site-map ### Schema tables -`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_title_words`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `medication_records`, `differential_records`, `source_review_events`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs` +`documents`, `document_pages`, `document_images`, `document_chunks`, `document_embedding_fields`, `document_index_units`, `document_table_facts`, `document_labels`, `document_summaries`, `document_sections`, `document_memory_cards`, `document_index_quality`, `document_title_words`, `document_publication_approvals`, `ingestion_jobs`, `ingestion_job_stages`, `indexing_v3_agent_jobs`, `import_batches`, `image_caption_cache`, `rag_queries`, `rag_query_misses`, `rag_aliases`, `rag_response_cache`, `rag_retrieval_logs`, `rag_visual_eval_cases`, `rag_visual_eval_runs`, `rag_answer_feedback`, `clinical_registry_records`, `clinical_registry_record_sources`, `medication_records`, `differential_records`, `source_review_events`, `user_favourites`, `user_preferences`, `api_rate_limits`, `api_rate_limit_subjects`, `audit_logs`, `storage_cleanup_jobs` **Storage buckets:** `clinical-documents`, `clinical-images` (private) diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 082fe0cd..b2420fb1 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -8,6 +8,14 @@ This document turns the current process review into phased, durable repo practic - **Provider-gated RAG safety ideas:** the same stale worktree contained conservative answer-quality thresholds, an evaluation cost-cap preflight, production-safety validation, deep-health assessment, and citation/vector proof tests. Its 754-line retrieval migration and route changes conflict with the later public-title privacy and migration chain and must not be replayed. If explicitly approved, rescope only the still-relevant preflight utilities and tests against current `main`; keep live OpenAI/Supabase validation separate. - **Semantic reranking rollout debt:** PR #901 keeps `RAG_SEMANTIC_RERANK_ENABLED=false`. Do not enable it until the provider-backed 36/36 retrieval-quality gate and an ambiguity-focused canary are explicitly approved and recorded. +## P3 residual-debt controls (2026-07-19) + +- `docs:check-index` is now a local and required static-PR guard; product-route and schema-table additions must update `docs/codebase-index.md` in the same change. +- `check:knip` blocks unused direct dependencies, unlisted or unresolved imports, and duplicate exports. Playwright's plugin is disabled because Knip already treats every `tests/**/*.spec.ts` file and both config files as entries, while executing the protected configs outside the repository runner is intentionally forbidden. `check:knip:exports` preserves the current 230 unused-export and 116 unused-type findings as advisory triage; do not bulk-delete those symbols without caller and runtime evidence. +- Unit coverage inventories pages/layouts, mockups, scripts, the TypeScript worker, and Supabase Edge Functions. The historical core-source threshold remains unchanged and scoped to its previous file set, so the broader zero-coverage inventory cannot dilute the established floor. +- The required Chromium PR job also runs the dashboard/document visual-artifact smoke. Quarantined and mockup journeys remain advisory, while full cross-browser coverage remains in the release matrix. +- `check:maintainability-budgets` prevents the four current hotspots from growing beyond their post-remediation 2026-07-19 baselines: `ClinicalDashboard.tsx` 4,272 lines, `rag.ts` 5,238, `DocumentViewer.tsx` 3,166, and `indexing-v3-agent/index.ts` 2,191. A budget breach must be handled by a cohesive, separately verified extraction; do not raise a ceiling to land unrelated behavior. + ## Staging tenancy evidence The provider-backed A/B tenancy regression is intentionally outside local and PR diff --git a/docs/testing.md b/docs/testing.md index 1230c07c..e6030804 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -15,7 +15,7 @@ Ordinary Vitest and Playwright runs remove OpenAI, Supabase, database, and E2E c | `npm run test:focused -- --files ` | Local iteration using Vitest related-file selection. It fails closed for deleted files, test infrastructure, configuration, or an empty/unsafe mapping. | | `npm run test` | Complete offline unit suite. | | `npm run test:live` | Explicit provider suite; requires `ALLOW_PROVIDER_TESTS=true`. | -| `npm run test:e2e:pr` | Required production Chromium journeys, excluding mockups and quarantined tests. | +| `npm run test:e2e:pr` | Required production Chromium journeys and visual-artifact smoke, excluding mockups and quarantined tests. | | `npm run test:e2e:advisory` | Quarantined and mockup journeys in one advisory invocation. | | `npm run verify:cheap` | Broad offline local gate: runtime/config checks, lint, typecheck, and the full unit suite. | | `npm run verify:pr-local` | PR-like local gate. Formatting is checked on the changed set, the full unit suite runs once, and RAG scope adds fixture/manifest validation. | @@ -35,4 +35,4 @@ Blocking tests run with zero retries. CI publishes list, JUnit, and JSON reports ## CI topology -PR CI keeps static checks separate from one required full unit run with coverage. UI scope uses one required production Chromium invocation for non-quarantined critical and regression journeys, plus one advisory invocation for quarantined and mockup journeys. Build, migration, security, and release behavior remain independently scoped and unchanged. +PR CI keeps static checks separate from one required full unit run with coverage. UI scope uses one required production Chromium invocation for non-quarantined critical, regression, and dashboard/document visual-artifact journeys, plus one advisory invocation for quarantined and mockup journeys. Build, migration, security, and release behavior remain independently scoped and unchanged. diff --git a/knip.json b/knip.json index b65040c1..556d1e15 100644 --- a/knip.json +++ b/knip.json @@ -9,8 +9,7 @@ ], "project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,mjs,cjs}", "worker/**/*.ts", "eslint-rules/**/*.mjs"], "ignore": ["src/lib/supabase/database.types.ts", "**/*mockup*", "supabase/functions/**"], - "ignoreDependencies": ["tailwindcss", "server-only"], - "ignoreBinaries": ["railway", "taskkill"], + "ignoreDependencies": ["tailwindcss"], "vitest": { "config": ["vitest.config.mts"] }, - "playwright": { "config": ["playwright.config.ts", "playwright.visual.config.ts"] } + "playwright": false } diff --git a/package-lock.json b/package-lock.json index cb24168a..6f13cb44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "hasInstallScript": true, "dependencies": { "@next/env": "16.2.10", - "@supabase/realtime-js": "^2.110.7", "@supabase/ssr": "^0.12.0", "@supabase/supabase-js": "^2.108.2", "exceljs": "^4.4.0", @@ -3310,12 +3309,6 @@ "node": ">=22.0.0" } }, - "node_modules/@supabase/phoenix": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", - "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", - "license": "MIT" - }, "node_modules/@supabase/postgrest-js": { "version": "2.110.2", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.110.2.tgz", @@ -3328,19 +3321,6 @@ "node": ">=22.0.0" } }, - "node_modules/@supabase/realtime-js": { - "version": "2.110.7", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.110.7.tgz", - "integrity": "sha512-AMtZjyFA2gsmjuxopPNS/sRznLQHG0Ht5x+ytTPTOh3vAcOTUlVRLx7gW4/CONNnbb3PKOkE+HmM35HOSbmomQ==", - "license": "MIT", - "dependencies": { - "@supabase/phoenix": "0.4.5", - "tslib": "2.8.1" - }, - "engines": { - "node": ">=22.0.0" - } - }, "node_modules/@supabase/ssr": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.12.0.tgz", diff --git a/package.json b/package.json index 667af8d6..ed261bb6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", "test:cross-tenant:staging": "node scripts/run-tsx.mjs scripts/test-cross-tenant-staging.ts", "verify:cheap": "node scripts/run-heavy.mjs --npm-script verify:cheap:internal", - "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run sitemap:check && npm run brand:check && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", + "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run sitemap:check && npm run docs:check-index && npm run check:knip && npm run check:maintainability-budgets && npm run brand:check && npm run check:therapy-data-index && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", "verify:pr-local": "node scripts/verify-pr-local.mjs", "verify:ui": "npm run check:runtime && npm run test:e2e:pr", "verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release", @@ -51,7 +51,9 @@ "ci:env-check": "node scripts/check-ci-env.mjs", "check:github-actions": "node scripts/check-github-action-pins.mjs", "check:client-bundle-secrets": "node scripts/check-client-bundle-secrets.mjs", - "check:knip": "knip", + "check:knip": "knip --no-progress --include dependencies,unlisted,unresolved,duplicates", + "check:knip:exports": "knip --no-progress --exports --no-exit-code", + "check:maintainability-budgets": "node scripts/check-maintainability-budgets.mjs", "check:ci-scope": "node scripts/ci-change-scope.mjs --self-test", "check:ci-triage": "node scripts/ci-triage.mjs --self-test", "check:pr-policy": "node scripts/pr-policy.mjs --self-test", @@ -174,7 +176,6 @@ }, "dependencies": { "@next/env": "16.2.10", - "@supabase/realtime-js": "^2.110.7", "@supabase/ssr": "^0.12.0", "@supabase/supabase-js": "^2.108.2", "exceljs": "^4.4.0", diff --git a/playwright.config.ts b/playwright.config.ts index 64f10400..4ba972e5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,14 +13,14 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; // they share a spec file. Every required browser project uses the same // production matcher and tag exclusion. const productionSpecPattern = - /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa|route-coverage))\.spec\.ts/; + /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa|route-coverage|visual-artifacts))\.spec\.ts/; const mockupSpecPattern = /.*ui-(tools|tools-collapse|tools-task-directory)\.spec\.ts/; const mockupTag = /@mockup/; export default defineConfig({ testDir: "./tests", testMatch: - /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa|route-coverage))\.spec\.ts/, + /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa|route-coverage|visual-artifacts))\.spec\.ts/, timeout: 60_000, retries: 0, // Fail the run if a stray `test.only` is committed: otherwise it silently diff --git a/scripts/check-maintainability-budgets.mjs b/scripts/check-maintainability-budgets.mjs new file mode 100644 index 00000000..878d373f --- /dev/null +++ b/scripts/check-maintainability-budgets.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { readFileSync } from "node:fs"; + +const budgets = new Map([ + ["src/components/ClinicalDashboard.tsx", 4272], + ["src/lib/rag.ts", 5238], + ["src/components/DocumentViewer.tsx", 3166], + ["supabase/functions/indexing-v3-agent/index.ts", 2191], +]); + +function sourceLineCount(file) { + const source = readFileSync(file, "utf8").replaceAll("\r\n", "\n"); + const lines = source.split("\n"); + return source.endsWith("\n") ? lines.length - 1 : lines.length; +} + +const failures = []; +for (const [file, maximum] of budgets) { + const actual = sourceLineCount(file); + if (actual > maximum) failures.push(`${file}: ${actual} lines exceeds the ${maximum}-line no-growth budget`); + else console.log(`[maintainability] ${file}: ${actual}/${maximum} lines`); +} + +if (failures.length) { + console.error("Maintainability hotspot budget exceeded. Extract a cohesive module instead of growing the monolith:"); + for (const failure of failures) console.error(` ${failure}`); + process.exit(1); +} + +console.log("Maintainability hotspot budgets passed."); diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 4b839f05..77cd4894 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -114,7 +114,6 @@ import { type DocumentDrawerStatusFilter, type DocumentPagination, type LabelReviewMutationBody, - recentQueryStorageKey, } from "@/components/clinical-dashboard/dashboard-contracts"; const DifferentialsHome = dynamic( @@ -171,7 +170,7 @@ const DocumentSearchResultsPanel = dynamic( { ssr: false }, ); -import { clearLegacyRecentQueries, demoRecentQueryOwnerId } from "@/components/clinical-dashboard/recent-query-storage"; +import { clearLegacyRecentQueries, demoRecentQueryOwnerId, recentQueryStorageKey } from "@/lib/recent-query-storage"; import type { SearchFacets } from "@/components/clinical-dashboard/document-search-results"; import { isWeakRelevance } from "@/components/clinical-dashboard/relevance"; import { diff --git a/src/components/clinical-dashboard/dashboard-contracts.ts b/src/components/clinical-dashboard/dashboard-contracts.ts index 2416ee0d..3d4c897b 100644 --- a/src/components/clinical-dashboard/dashboard-contracts.ts +++ b/src/components/clinical-dashboard/dashboard-contracts.ts @@ -5,8 +5,6 @@ export const navigationHashes = ["#search", "#quotes", "#images", "#sources"] as export const mobileSectionFabMediaQuery = "(max-width: 768px), ((max-width: 1023px) and (hover: none) and (pointer: coarse))"; -export const recentQueryStorageKey = "clinical-kb-recent-queries"; - export type DocumentPagination = { limit: number; offset: number; diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index ca61aba7..88b59cf8 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -15,11 +15,7 @@ import { import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setup-dialog"; import { ClinicalDashboard } from "@/components/ClinicalDashboard"; -import { - clearLegacyRecentQueries, - demoRecentQueryOwnerId, - loadRecentQueries, -} from "@/components/clinical-dashboard/recent-query-storage"; +import { clearLegacyRecentQueries, demoRecentQueryOwnerId, loadRecentQueries } from "@/lib/recent-query-storage"; import { PatientProfileProvider } from "@/components/clinical-dashboard/patient-profile-context"; import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context"; import { SettingsDialog } from "@/components/clinical-dashboard/settings-dialog"; diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index cc5bdd92..3496d5dc 100644 --- a/src/components/clinical-dashboard/settings-dialog.tsx +++ b/src/components/clinical-dashboard/settings-dialog.tsx @@ -42,7 +42,7 @@ import { POPULATION_OPTIONS, useAppPreferences, } from "@/components/clinical-dashboard/use-app-preferences"; -import { clearRecentQueries, countRecentQueries } from "@/components/clinical-dashboard/recent-query-storage"; +import { clearRecentQueries, countRecentQueries } from "@/lib/recent-query-storage"; import { cn, fieldControlWithIcon, diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 414c2735..52ef044f 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -170,6 +170,37 @@ function detailRowsFor(form: FormRecord) { ].filter((row) => hasText(row.value)); } +export function formDetailsClipboardText(form: FormRecord) { + const lines = [form.title, `Form code: ${formCode(form)}`]; + + if (hasText(form.subtitle)) lines.push(displayText(form.subtitle)); + const statuses = (form.statusChips ?? []).map((chip) => chip.label?.trim()).filter(hasText); + if (statuses.length) lines.push(`Status: ${statuses.join(", ")}`); + + for (const card of summaryCardsFor(form)) { + const label = displayText(card.label, "Priority fact"); + const values = [card.title, card.detail].filter(hasText).map((value) => value.trim()); + if (values.length) lines.push(`${label}: ${values.join(" — ")}`); + } + + if (hasText(form.bestUse)) lines.push(`Legal boundary: ${displayText(form.bestUse)}`); + for (const row of detailRowsFor(form)) lines.push(`${row.label}: ${displayText(row.value)}`); + + const primaryContact = hasText(form.primaryContact?.value) + ? form.primaryContact + : form.contacts?.find((contact) => hasText(contact.value)); + if (primaryContact) { + lines.push(`${displayText(primaryContact.label, "Contact")}: ${displayText(primaryContact.value)}`); + } + + if (hasText(form.source?.label)) lines.push(`Source: ${displayText(form.source.label)}`); + if (hasText(form.source?.status)) lines.push(`Source status: ${displayText(form.source.status)}`); + if (hasText(form.source?.reviewed)) lines.push(`Source reviewed: ${displayText(form.source.reviewed)}`); + if (hasText(form.source?.url)) lines.push(`Source URL: ${displayText(form.source.url)}`); + + return lines.join("\n"); +} + function callHref(contact: ServiceContact | null) { if (!contact || contact.kind !== "phone" || !hasText(contact.value)) return null; return `tel:${contact.value.replace(/[^\d+]/g, "")}`; @@ -690,7 +721,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) {
copyValue(primaryContact?.value ?? form.title, "Form detail copied")} + onCopy={() => copyValue(formDetailsClipboardText(form), "Form details copied")} hrefForCall={hrefForCall} />
@@ -748,7 +779,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) { copyValue(primaryContact?.value ?? form.title, "Form detail copied")} + onCopy={() => copyValue(formDetailsClipboardText(form), "Form details copied")} hrefForCall={hrefForCall} /> diff --git a/src/components/forms/forms-search-results-page.tsx b/src/components/forms/forms-search-results-page.tsx index cb24e2ed..eb978d0f 100644 --- a/src/components/forms/forms-search-results-page.tsx +++ b/src/components/forms/forms-search-results-page.tsx @@ -314,7 +314,7 @@ function ResultsTable({
, ); -// Visually identical to CompassIcon; alias so the two can't drift. -export const TargetIcon = CompassIcon; export const CrosshairIcon = makeIcon( <> diff --git a/src/components/therapy-compass/screens/detail-screen.tsx b/src/components/therapy-compass/screens/detail-screen.tsx index a983249e..54616e8e 100644 --- a/src/components/therapy-compass/screens/detail-screen.tsx +++ b/src/components/therapy-compass/screens/detail-screen.tsx @@ -12,6 +12,7 @@ import { ChecklistIcon, ChevronRightIcon, ClockIcon, + CompassIcon, CrosshairIcon, DatabaseIcon, FileTextIcon, @@ -19,7 +20,6 @@ import { PersonIcon, ScaleIcon, ShieldIcon, - TargetIcon, } from "../icons"; import { Eyebrow, LoadingState, StatusBadge, TagRow } from "../ui"; @@ -152,7 +152,7 @@ export function DetailScreen() {
At a glance
- + { expect(formsSearchSource).toContain("Content match in record details"); expect(formsSearchSource).not.toContain("Content match in related pathway"); expect(formsSearchSource).toContain("View all forms"); + expect(formsSearchSource).toContain('appModeHomeHref("forms", { query, focus: true, run: true })'); + expect(formsSearchSource).not.toContain('href="/forms"'); expect(formsHomeSource).not.toMatch(/Source verified|Open account setup/); expect(formsHomeSource).not.toMatch( /Number, pathway, clock|Maker, clock, copies|Browse pathways|Before, current, parallel, after|starter set of MHA 2014 forms|follow a pathway/, diff --git a/tests/forms.test.ts b/tests/forms.test.ts index b5521eea..cbf77de7 100644 --- a/tests/forms.test.ts +++ b/tests/forms.test.ts @@ -4,11 +4,27 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { formDetailsClipboardText } from "@/components/forms/form-detail-page"; import { formCatalogDetails } from "@/lib/form-catalog"; import { defaultFormSlug, formRecords, formStaticParams, getFormRecord, searchFormRecords } from "@/lib/forms"; import { buildDefaultFormRows } from "@/lib/registry-fixtures"; describe("psychiatry form records", () => { + it("copies the visible form details rather than only the primary contact", () => { + const form = formRecords.find((record) => record.primaryContact?.value && record.bestUse && record.source?.status); + if (!form?.primaryContact?.value || !form.bestUse || !form.source?.status) { + throw new Error("Expected a form fixture with copyable details"); + } + + const copied = formDetailsClipboardText(form); + expect(copied).toContain(form.title); + expect(copied).toContain(`Form code: ${formCatalogDetails(form)?.form}`); + expect(copied).toContain(`Legal boundary: ${form.bestUse}`); + expect(copied).toContain(form.primaryContact.value); + expect(copied).toContain(`Source status: ${form.source.status}`); + expect(copied.trim()).not.toBe(form.primaryContact.value.trim()); + }); + it("covers every entry on the current WA MHA 2014 forms register", () => { expect(formRecords).toHaveLength(54); const details = formRecords.map(formCatalogDetails); diff --git a/tests/lib-layering.test.ts b/tests/lib-layering.test.ts new file mode 100644 index 00000000..55aa2851 --- /dev/null +++ b/tests/lib-layering.test.ts @@ -0,0 +1,24 @@ +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +function sourceFiles(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) return sourceFiles(fullPath); + return /\.[cm]?[jt]sx?$/.test(entry.name) ? [fullPath] : []; + }); +} + +describe("library layering", () => { + it("keeps src/lib independent from UI components", () => { + const libRoot = path.resolve("src/lib"); + const reverseImports = sourceFiles(libRoot).flatMap((file) => { + const source = readFileSync(file, "utf8"); + return /(?:from\s+|import\s*\()\s*["']@\/components\//.test(source) ? [path.relative(process.cwd(), file)] : []; + }); + + expect(reverseImports).toEqual([]); + }); +}); diff --git a/tests/recent-query-storage.test.ts b/tests/recent-query-storage.test.ts index 571f73f4..e1c9ac9a 100644 --- a/tests/recent-query-storage.test.ts +++ b/tests/recent-query-storage.test.ts @@ -1,12 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { recentQueryStorageKey } from "@/components/clinical-dashboard/dashboard-contracts"; import { clearLegacyRecentQueries, clearRecentQueries, demoRecentQueryOwnerId, loadRecentQueries, -} from "@/components/clinical-dashboard/recent-query-storage"; + recentQueryStorageKey, +} from "@/lib/recent-query-storage"; describe("recent query storage", () => { let localStore: Map; diff --git a/tests/test-runner-safety.test.ts b/tests/test-runner-safety.test.ts index 72ea7123..7db49671 100644 --- a/tests/test-runner-safety.test.ts +++ b/tests/test-runner-safety.test.ts @@ -145,6 +145,21 @@ describe("provider-safe test environment", () => { expect(config).toContain('exclude: liveProviderTests ? [] : ["tests/**/*.live.test.ts"]'); }); + it("keeps residual source surfaces visible without lowering the core coverage floor", () => { + const config = readFileSync(new URL("../vitest.config.mts", import.meta.url), "utf8"); + for (const pattern of [ + '"src/**/*.{ts,tsx}"', + '"scripts/**/*.{ts,mjs,cjs}"', + '"worker/**/*.ts"', + '"supabase/functions/**/*.ts"', + ]) { + expect(config).toContain(pattern); + } + expect(config).toContain('"src/{lib/**/*.ts,app/**/route.ts,components/**/*.{ts,tsx}}"'); + expect(config).not.toContain('"src/app/**/{page,layout,loading,error,not-found}.tsx"'); + expect(config).not.toContain('"src/**/*mockup*"'); + }); + it("refuses the live-test command before collection when permission is absent", () => { const result = spawnSync(process.execPath, ["scripts/run-live-tests.mjs"], { cwd: path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), @@ -168,6 +183,7 @@ describe("provider-safe test environment", () => { const runner = readFileSync(new URL("../scripts/run-playwright.mjs", import.meta.url), "utf8"); const baseUrl = readFileSync(new URL("../scripts/playwright-base-url.ts", import.meta.url), "utf8"); const ragRunner = readFileSync(new URL("../scripts/eval-rag-offline.mjs", import.meta.url), "utf8"); + const playwrightConfig = readFileSync(new URL("../playwright.config.ts", import.meta.url), "utf8"); const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { scripts: Record; }; @@ -186,6 +202,7 @@ describe("provider-safe test environment", () => { expect(packageJson.scripts["test:e2e:regression"]).toContain('--grep-invert "@critical|@quarantine|@mockup"'); expect(baseUrl.indexOf("if (!allowEnsure)")).toBeLessThan(baseUrl.indexOf("findExistingLocalProjectUrl();")); expect(ragRunner).toContain("cwd: projectRoot"); + expect(playwrightConfig).toContain("visual-artifacts"); }); it("uses webpack when shared worktree dependencies resolve outside the project", () => { diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index a25bd90b..d9a2264c 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1,7 +1,6 @@ import type { Route } from "playwright-core"; import { expect, test, type Locator, type Page } from "playwright/test"; import { scrollPrimarySurface } from "./playwright-scroll"; -import { recentQueryStorageKey } from "../src/components/clinical-dashboard/dashboard-contracts"; import { answerThreadStorageKey } from "../src/lib/answer-thread-storage"; import { documentSummaryQuestion } from "../src/lib/answer-contract"; import { demoAnswer, demoDocuments, demoSummary, getDemoDocument, getDemoDocumentPayload } from "../src/lib/demo-data"; @@ -10,6 +9,7 @@ import { deriveGovernanceFromSections } from "../src/lib/medication-records"; import { getMedicationRecord, loadMedicationSnapshot } from "../src/lib/medication-snapshot"; import { medicationToSearchResult, rankMedicationRecords, type MedicationRecord } from "../src/lib/medications"; import { serviceRecords } from "../src/lib/services"; +import { recentQueryStorageKey } from "../src/lib/recent-query-storage"; const dashboardViewports = [ { name: "small-mobile", width: 320, height: 720 }, diff --git a/vitest.config.mts b/vitest.config.mts index adb5f518..c9848408 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -15,20 +15,19 @@ const config = { provider: "v8", reporter: ["text", "lcov"], reportsDirectory: "coverage", - include: ["src/lib/**/*.ts", "src/app/**/route.ts", "src/components/**/*.{ts,tsx}"], - exclude: [ - "src/app/**/{page,layout,loading,error,not-found}.tsx", - // Design-exploration mockups are dev-only prototypes (see mockups/README.md). - "src/**/*mockup*", - "src/app/mockups/**", - ], - // Regression floor set just below current coverage. Raise over time; the point - // is to fail CI on a meaningful drop, not to chase a target. + // Inventory every executable TypeScript surface, including pages/layouts, + // mockups, scripts, the worker, and Supabase Edge Functions. The existing + // core threshold remains scoped to its historical files so expanding the + // inventory cannot weaken that regression floor. + include: ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,mjs,cjs}", "worker/**/*.ts", "supabase/functions/**/*.ts"], + exclude: ["src/lib/supabase/database.types.ts"], thresholds: { - statements: 48, - branches: 38, - functions: 43, - lines: 50, + "src/{lib/**/*.ts,app/**/route.ts,components/**/*.{ts,tsx}}": { + statements: 48, + branches: 38, + functions: 43, + lines: 50, + }, }, }, // Two projects run under one `npm run test` invocation. `extends: true` makes