From 9b4b3056b1f4ef7355e8947d6b210dee63de182e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 19 Aug 2026 02:19:50 +0800 Subject: [PATCH 1/2] feat(therapy): ship Therapy in production with its review state disclosed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Therapy was hidden from users by three independent production gates, only the first of which was visible: - `devOnly: true` in `app-modes.ts` removed it from every mode list. - The `/therapy-compass` route layout returned `notFound()` in production. - `therapyRecordsForEnvironment` filtered out every record whose `reviewStatus` was not `reviewed` — all 205 of them — so each detail/brief/sheet route and every universal-search therapy hit 404'd for real users while working locally. Removing only the first would have shipped a Therapy mode onto an empty library, so all three go together. The gates existed because the catalogue awaits qualified-clinician sign-off. The owner's decision is to disclose that state rather than hide the library: - `TherapyReviewNotice` sits above the Therapy home hero — non-interactive and not dismissible, with counts read from the generated catalogue summary so the wording tracks the data as records are signed off. - The generator now emits `needsReviewCount` and its check mode compares it, so the notice cannot drift away from the records it describes. - Every existing per-record `reviewStatus` badge is retained: result cards, detail pages, briefs, patient sheets, comparisons, pathways, and the universal-search "Needs source review" badge. `therapyNeedsReview` survives as the label source only; it no longer gates reachability anywhere. The `PLAYWRIGHT_OFFLINE_MODE` bypass is retired with the gate it existed to reach, so no half-restored gate can pass locally and 404 in production. The contracts that pinned the old behaviour now pin the new one: reachability in `app-modes.test.ts`, the notice and per-record badges in `therapy-review-regressions.test.ts`, and the retired bypass in `therapy-pr-unblocking-contract.test.ts`. Co-Authored-By: Claude Opus 5 --- docs/codebase-index.md | 1 + scripts/build-therapies-index.mjs | 7 +++ .../(search-app)/therapy-compass/layout.tsx | 19 +++---- .../therapy-compass/data/generated-assets.ts | 1 + .../therapy-compass/screens/home-screen.tsx | 8 ++- .../therapy-compass/therapy-review-notice.tsx | 49 ++++++++++++++++ src/lib/app-modes.ts | 11 ++-- src/lib/therapies.ts | 57 +++++++++---------- tests/app-modes.test.ts | 14 +++-- tests/therapy-pr-unblocking-contract.test.ts | 17 ++++-- tests/therapy-ranking.test.ts | 20 ++++--- tests/therapy-review-regressions.test.ts | 49 ++++++++++++++-- 12 files changed, 181 insertions(+), 72 deletions(-) create mode 100644 src/components/therapy-compass/therapy-review-notice.tsx diff --git a/docs/codebase-index.md b/docs/codebase-index.md index 00387832ee..afddf8346a 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -64,6 +64,7 @@ Smaller top-level directories that are easy to miss: - **Home:** `src/app/(search-app)/page.tsx` — dashboard rendered by shell - **Dashboard:** `src/components/ClinicalDashboard.tsx` + `src/components/clinical-dashboard/` - **Modes (15):** `src/lib/app-modes.ts` — answer, documents, services, forms, favourites, differentials, DSM-5 diagnosis, specifiers, formulation, prescribing, tools, calculators, Therapy, Factsheets, Dictionary + - **Therapy review disclosure.** Therapy was `devOnly` while its 205-record catalogue awaited qualified-clinician sign-off. That hid the mode from production navigation, 404'd `/therapy-compass` in the route layout, and made `therapyRecordsForEnvironment` filter every record out — so all 205 detail/brief/sheet routes and every universal-search therapy hit 404'd for real users while working locally. The owner's decision (2026-08-19) replaced the gate with disclosure: reachability is no longer conditioned on review status anywhere, and the caveat is stated instead — catalogue-wide by `TherapyReviewNotice` above the Therapy home hero (counts from the generated `THERAPY_CATALOGUE_SUMMARY.needsReviewCount`, kept in step by the index generator's check mode), and per record by the `reviewStatus` badge on every card, detail page, brief, sheet, comparison, pathway, and universal-search result. `therapyNeedsReview` survives as the label source only. Pinned by `tests/app-modes.test.ts` (reachability), `tests/therapy-review-regressions.test.ts` (the notice and the per-record badges), and `tests/therapy-pr-unblocking-contract.test.ts` (the retired `PLAYWRIGHT_OFFLINE_MODE` bypass that existed only to reach the gated route). ### Product pages (`src/app/`) diff --git a/scripts/build-therapies-index.mjs b/scripts/build-therapies-index.mjs index e66efaca8e..dca56b27e5 100644 --- a/scripts/build-therapies-index.mjs +++ b/scripts/build-therapies-index.mjs @@ -86,6 +86,7 @@ function renderManifest(current, previous, summary) { "// not download and parse a record projection before its LCP can paint.", "export const THERAPY_CATALOGUE_SUMMARY = {", ` totalCount: ${summary.totalCount},`, + ` needsReviewCount: ${summary.needsReviewCount},`, ` defaultBriefSlug: ${JSON.stringify(summary.defaultBriefSlug)},`, ` defaultSheetSlug: ${JSON.stringify(summary.defaultSheetSlug)},`, "} as const;\n", @@ -152,6 +153,10 @@ const browserHomeProjected = therapies const catalogueSummary = { totalCount: browserHomeProjected.length, + // Records still awaiting qualified-clinician sign-off. Therapy ships its review + // state rather than hiding unreviewed records, so the library notice states this + // count; computing it here keeps the notice from drifting away from the data. + needsReviewCount: browserHomeProjected.filter((therapy) => therapy.reviewStatus !== "reviewed").length, defaultBriefSlug: browserHomeProjected.find((therapy) => therapy.briefInterventionAvailable)?.slug ?? null, defaultSheetSlug: browserHomeProjected.find((therapy) => therapy.patientSheetAvailable)?.slug ?? null, }; @@ -160,11 +165,13 @@ if (checkOnly) { const summaryBlock = extractConstObjectBody(currentManifest, "THERAPY_CATALOGUE_SUMMARY"); const recordedSummary = { totalCount: Number(summaryBlock.match(/totalCount: (\d+)/)?.[1] ?? Number.NaN), + needsReviewCount: Number(summaryBlock.match(/needsReviewCount: (\d+)/)?.[1] ?? Number.NaN), defaultBriefSlug: summaryBlock.match(/defaultBriefSlug: (null|"[^"]+")/)?.[1] ?? "", defaultSheetSlug: summaryBlock.match(/defaultSheetSlug: (null|"[^"]+")/)?.[1] ?? "", }; if ( recordedSummary.totalCount !== catalogueSummary.totalCount || + recordedSummary.needsReviewCount !== catalogueSummary.needsReviewCount || recordedSummary.defaultBriefSlug !== JSON.stringify(catalogueSummary.defaultBriefSlug) || recordedSummary.defaultSheetSlug !== JSON.stringify(catalogueSummary.defaultSheetSlug) ) { diff --git a/src/app/(search-app)/therapy-compass/layout.tsx b/src/app/(search-app)/therapy-compass/layout.tsx index 89d0c498f3..ff162c047d 100644 --- a/src/app/(search-app)/therapy-compass/layout.tsx +++ b/src/app/(search-app)/therapy-compass/layout.tsx @@ -1,25 +1,22 @@ import { Suspense } from "react"; import type { ReactNode } from "react"; -import { notFound } from "next/navigation"; import { ModeHomeRouteLoading } from "@/components/mode-home-page-skeleton"; import { TherapyCompassRouteLayout } from "@/components/therapy-compass/therapy-compass-route-layout"; -import { isAppModeVisible } from "@/lib/app-modes"; // Therapy-only state belongs at the deepest shared route segment. Keeping this // provider out of the global search shell prevents every other mode from // downloading Therapy's client graph, while the client boundary can still read // current pathname/search params on each navigation. +// +// This segment previously returned a not-found response in production, because +// Therapy was a dev-only mode while its catalogue awaited qualified-clinician +// sign-off. That gate is gone: the review state is now disclosed on the library +// notice and on every record instead of removing the mode, so this layout has no +// environment branch at all. See `src/lib/therapies.ts` and the contracts in +// tests/therapy-review-regressions.test.ts, which assert the gate stays absent +// by scanning this file — keep the prose here free of the literal call. export default function TherapyCompassLayout({ children }: { children: ReactNode }) { - const offlineReviewBuild = process.env.PLAYWRIGHT_OFFLINE_MODE === "true"; - if ( - process.env.NODE_ENV === "production" && - !offlineReviewBuild && - !isAppModeVisible("therapy-compass", "production") - ) { - notFound(); - } - return ( }> {children} diff --git a/src/components/therapy-compass/data/generated-assets.ts b/src/components/therapy-compass/data/generated-assets.ts index 103044ac17..10af4dda60 100644 --- a/src/components/therapy-compass/data/generated-assets.ts +++ b/src/components/therapy-compass/data/generated-assets.ts @@ -21,6 +21,7 @@ export const THERAPY_CATALOGUE_ASSETS_PREVIOUS = { // not download and parse a record projection before its LCP can paint. export const THERAPY_CATALOGUE_SUMMARY = { totalCount: 205, + needsReviewCount: 205, defaultBriefSlug: "acceptance-and-commitment-therapy-act", defaultSheetSlug: "acceptance-and-commitment-therapy-act", } as const; diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index 474ccd72ae..08db647fa5 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -7,6 +7,8 @@ import { ModeHomeMain, ModeHomeTemplate, ModeHomeVerificationFooter } from "@/co import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { therapyHrefWithSearchParams, therapyScreenHref } from "@/lib/therapy-compass-navigation"; +import { TherapyReviewNotice } from "../therapy-review-notice"; + import { THERAPY_CATALOGUE_SUMMARY } from "../data/generated-assets"; const SUGGESTIONS = [ @@ -28,6 +30,10 @@ export function HomeScreen() { return ( + {/* Above the hero, not in the footer: the catalogue-wide review caveat is + the first thing a reader of this library needs, and the quiet footer + line is not load-bearing enough to carry it alone. */} + } /> diff --git a/src/components/therapy-compass/therapy-review-notice.tsx b/src/components/therapy-compass/therapy-review-notice.tsx new file mode 100644 index 0000000000..4943e21495 --- /dev/null +++ b/src/components/therapy-compass/therapy-review-notice.tsx @@ -0,0 +1,49 @@ +import { ShieldAlert } from "lucide-react"; + +import { cn } from "@/components/ui-primitives"; + +import { THERAPY_CATALOGUE_SUMMARY } from "./data/generated-assets"; + +/** + * Library-level review disclosure for Therapy. + * + * Therapy is reachable in production with its review state stated rather than + * hidden — the mode was previously `devOnly`, which 404'd the route and all 205 + * records for real users. This notice carries the catalogue-wide half of that + * disclosure; the per-record half is the badge every result card, detail page, + * brief and patient sheet already renders from `reviewStatus`. + * + * Counts come from the generated catalogue summary, which the index generator's + * check mode keeps in step with the data, so the wording cannot drift as records + * are signed off. Non-interactive on purpose: no tap target, no link, nothing to + * dismiss — a caveat the reader can turn off is not a caveat. + */ +export function TherapyReviewNotice({ className }: { className?: string }) { + const total: number = THERAPY_CATALOGUE_SUMMARY.totalCount; + const needsReview: number = THERAPY_CATALOGUE_SUMMARY.needsReviewCount; + + if (needsReview === 0) return null; + + const scope = + needsReview === total + ? `No therapy record in this library has completed clinician review yet.` + : `${needsReview} of ${total} therapy records have not completed clinician review yet.`; + + return ( +
+ +

+ Awaiting clinician review. + {scope} Records are shown so they can be read and checked against their cited sources — verify each one before + using it clinically. +

+
+ ); +} diff --git a/src/lib/app-modes.ts b/src/lib/app-modes.ts index 00193ba8ae..9162919a62 100644 --- a/src/lib/app-modes.ts +++ b/src/lib/app-modes.ts @@ -368,10 +368,13 @@ export const appModeDefinitions = [ label: "Therapy", description: "Source-grounded therapy decision support", href: "/therapy-compass", - // Keep Therapy available for local clinical review while its catalogue is - // awaiting qualified-clinician sign-off. Removing this gate requires the - // catalogue review-status contract to prove production-ready records. - devOnly: true, + // Therapy ships in production with its review state disclosed rather than + // hidden. It was previously `devOnly`, which 404'd the route and every + // record for real users; the owner's decision is that a catalogue labelled + // "needs source review" on the library notice, every result card and every + // record page is more useful — and no less honest — than an absent mode. + // Per-record sign-off is still tracked by `therapyNeedsReview` and surfaced + // everywhere the record appears; it no longer gates reachability. search: { kind: "therapies", // The longer phrase became the late portal's LCP element on Therapy Home. diff --git a/src/lib/therapies.ts b/src/lib/therapies.ts index 3cfb5eb0ce..3ed946fc89 100644 --- a/src/lib/therapies.ts +++ b/src/lib/therapies.ts @@ -28,43 +28,33 @@ export const therapyRecords = therapiesIndexJson as TherapyIndexRecord[]; const bySlug = new Map(therapyRecords.map((record) => [record.slug, record])); -function defaultTherapyEnvironment(): string | undefined { - return process.env.PLAYWRIGHT_OFFLINE_MODE === "true" ? "development" : process.env.NODE_ENV; -} - -/** Production may expose only records that have completed clinical review. */ -export function therapyRecordsForEnvironment(environment = defaultTherapyEnvironment()): TherapyIndexRecord[] { - return environment === "production" ? therapyRecords.filter((record) => !therapyNeedsReview(record)) : therapyRecords; -} +// The whole catalogue is reachable in every environment. Reachability is +// deliberately NOT gated on review status: a record awaiting qualified-clinician +// sign-off is disclosed as such (`therapyNeedsReview` drives the library notice, +// the result-card badge and the record-page badge) rather than hidden. The +// previous production filter removed all 205 records, so every Therapy route and +// every universal-search therapy hit 404'd for real users while working locally. -export function findTherapyRecord( - slug: string, - environment = defaultTherapyEnvironment(), -): TherapyIndexRecord | undefined { - const record = bySlug.get(slug); - return record && (environment !== "production" || !therapyNeedsReview(record)) ? record : undefined; +export function findTherapyRecord(slug: string): TherapyIndexRecord | undefined { + return bySlug.get(slug); } -export function therapyRecordExists(slug: string, environment = defaultTherapyEnvironment()): boolean { - return Boolean(findTherapyRecord(slug, environment)); +export function therapyRecordExists(slug: string): boolean { + return bySlug.has(slug); } -export function therapySlugs(environment = defaultTherapyEnvironment()): string[] { - return therapyRecordsForEnvironment(environment).map((record) => record.slug); +export function therapySlugs(): string[] { + return therapyRecords.map((record) => record.slug); } /** Slugs whose record ships a brief-intervention version (the rest 404 that route). */ -export function therapyBriefSlugs(environment = defaultTherapyEnvironment()): string[] { - return therapyRecordsForEnvironment(environment) - .filter((record) => record.briefInterventionAvailable) - .map((record) => record.slug); +export function therapyBriefSlugs(): string[] { + return therapyRecords.filter((record) => record.briefInterventionAvailable).map((record) => record.slug); } /** Slugs whose record ships a patient sheet (the rest 404 that route). */ -export function therapySheetSlugs(environment = defaultTherapyEnvironment()): string[] { - return therapyRecordsForEnvironment(environment) - .filter((record) => record.patientSheetAvailable) - .map((record) => record.slug); +export function therapySheetSlugs(): string[] { + return therapyRecords.filter((record) => record.patientSheetAvailable).map((record) => record.slug); } /** True when a therapy still awaits qualified-clinician sign-off. */ @@ -72,13 +62,18 @@ export function therapyNeedsReview(record: TherapyIndexRecord): boolean { return record.reviewStatus !== "reviewed"; } +/** Count of records still awaiting sign-off — drives the library review notice. */ +export function therapyNeedsReviewCount(): number { + return therapyRecords.filter(therapyNeedsReview).length; +} + export type TherapySearchMatch = { record: TherapyIndexRecord; score: number }; /** - * Rank the production-safe therapy library through the shared Therapy scorer. - * An empty query returns the alphabetical library (stable order) so the - * universal-search domain can still surface a browse list. + * Rank the therapy library through the shared Therapy scorer. An empty query + * returns the alphabetical library (stable order) so the universal-search + * domain can still surface a browse list. */ -export function searchTherapyRecords(query: string, environment = defaultTherapyEnvironment()): TherapySearchMatch[] { - return rankTherapyCandidates(therapyRecordsForEnvironment(environment), query); +export function searchTherapyRecords(query: string): TherapySearchMatch[] { + return rankTherapyCandidates(therapyRecords, query); } diff --git a/tests/app-modes.test.ts b/tests/app-modes.test.ts index a4ae603758..0986369a41 100644 --- a/tests/app-modes.test.ts +++ b/tests/app-modes.test.ts @@ -316,7 +316,7 @@ describe("app mode search contract", () => { expect(isAppModeVisible("prescribing", "production")).toBe(true); expect(isAppModeVisible("tools", "production")).toBe(true); expect(isAppModeVisible("calculators", "production")).toBe(true); - expect(isAppModeVisible("therapy-compass", "production")).toBe(false); + expect(isAppModeVisible("therapy-compass", "production")).toBe(true); expect(isAppModeVisible("factsheets", "production")).toBe(true); expect(productionModes).not.toContain("evidence"); expect(productionModes).toContain("services"); @@ -329,7 +329,7 @@ describe("app mode search contract", () => { expect(productionModes).toContain("prescribing"); expect(productionModes).toContain("tools"); expect(productionModes).toContain("calculators"); - expect(productionModes).not.toContain("therapy-compass"); + expect(productionModes).toContain("therapy-compass"); expect(productionModes).toContain("factsheets"); expect(developmentModes).toEqual( expect.arrayContaining([ @@ -352,12 +352,16 @@ describe("app mode search contract", () => { expect(developmentModes).not.toContain("evidence"); }); - it("keeps Therapy Compass behind clinical review in production", () => { + // Therapy was `devOnly` while its catalogue awaited clinician sign-off, which + // hid the mode and 404'd all 205 records for real users. The owner's decision + // is to ship it with its review state disclosed instead, so production and + // development must now agree — a re-added `devOnly: true` fails here. + it("keeps Therapy reachable in production with its review state disclosed, not hidden", () => { expect(isAppModeId("therapy-compass")).toBe(true); expect(isAppModeVisible("therapy-compass", "development")).toBe(true); - expect(isAppModeVisible("therapy-compass", "production")).toBe(false); + expect(isAppModeVisible("therapy-compass", "production")).toBe(true); expect(visibleAppModeDefinitions("development").map((mode) => mode.id)).toContain("therapy-compass"); - expect(visibleAppModeDefinitions("production").map((mode) => mode.id)).not.toContain("therapy-compass"); + expect(visibleAppModeDefinitions("production").map((mode) => mode.id)).toContain("therapy-compass"); }); it("gates Favourites mode to authenticated or demo sessions", () => { diff --git a/tests/therapy-pr-unblocking-contract.test.ts b/tests/therapy-pr-unblocking-contract.test.ts index 9b806fc7c4..83bbda7ef7 100644 --- a/tests/therapy-pr-unblocking-contract.test.ts +++ b/tests/therapy-pr-unblocking-contract.test.ts @@ -10,16 +10,21 @@ describe("Therapy PR unblocking contracts", () => { expect(source).toContain('import("@/lib/therapies")'); }); - it("keeps the production content gate while allowing isolated offline UI verification", () => { + // The Therapy production content gate is gone (see tests/app-modes.test.ts and + // tests/therapy-review-regressions.test.ts). Its PLAYWRIGHT_OFFLINE_MODE bypass + // existed only to let offline UI verification reach the gated route, so it must + // go with it: a bypass left behind an absent gate is how a half-restored gate + // ends up passing locally and 404ing in production. + it("retires the offline bypass along with the Therapy production content gate", () => { const layoutSource = read("src/app/(search-app)/therapy-compass/layout.tsx"); const therapiesSource = read("src/lib/therapies.ts"); - expect(layoutSource).toContain('process.env.PLAYWRIGHT_OFFLINE_MODE === "true"'); - expect(layoutSource).toContain("!offlineReviewBuild"); - expect(layoutSource).toContain("notFound()"); + expect(layoutSource).not.toContain("PLAYWRIGHT_OFFLINE_MODE"); + expect(layoutSource).not.toContain("offlineReviewBuild"); + expect(layoutSource).not.toContain("notFound()"); expect(layoutSource).not.toContain("NEXT_PUBLIC_DEMO_MODE"); - expect(therapiesSource).toContain('process.env.PLAYWRIGHT_OFFLINE_MODE === "true" ? "development"'); - expect(therapiesSource).toContain('environment === "production"'); + expect(therapiesSource).not.toContain("PLAYWRIGHT_OFFLINE_MODE"); + expect(therapiesSource).not.toContain('environment === "production"'); }); it("canonicalises hidden shared-home modes instead of retaining impossible URL state", () => { diff --git a/tests/therapy-ranking.test.ts b/tests/therapy-ranking.test.ts index 5b7d0ff215..45c6c41f40 100644 --- a/tests/therapy-ranking.test.ts +++ b/tests/therapy-ranking.test.ts @@ -8,8 +8,8 @@ import { rankTherapyCandidates, scoreTherapyCandidate } from "@/lib/therapy-rank import { findTherapyRecord, searchTherapyRecords, + therapyNeedsReview, therapyRecords, - therapyRecordsForEnvironment, therapySlugs, } from "@/lib/therapies"; @@ -50,12 +50,16 @@ describe("shared Therapy ranker", () => { expect(rankTherapyCandidates(records, "CBT")[0]?.record.name).toBe("Cognitive behavioural therapy"); }); - it("excludes unreviewed Therapy content from production discovery and routes", () => { + // The inverse of this used to hold: production filtered every unreviewed record + // out, which emptied discovery and 404'd all 205 routes. Review status is now a + // disclosure, not a reachability gate, so an unreviewed record must resolve. + it("keeps unreviewed Therapy content discoverable and routable, flagged rather than dropped", () => { expect(therapyRecords.length).toBeGreaterThan(0); - expect(therapyRecordsForEnvironment("production")).toEqual([]); - expect(searchTherapyRecords("CBT", "production")).toEqual([]); - expect(therapySlugs("production")).toEqual([]); - expect(findTherapyRecord(therapyRecords[0].slug, "production")).toBeUndefined(); + const unreviewed = therapyRecords.find(therapyNeedsReview); + expect(unreviewed).toBeDefined(); + expect(therapySlugs()).toContain(unreviewed!.slug); + expect(findTherapyRecord(unreviewed!.slug)).toBe(unreviewed); + expect(searchTherapyRecords("CBT").length).toBeGreaterThan(0); }); it.each([ @@ -74,7 +78,7 @@ describe("shared Therapy ranker", () => { const catalogueOrder = searchTherapies(fullTherapyRecords, { ...EMPTY_SEARCH, query }) .slice(0, 5) .map((record) => record.slug); - const universalOrder = searchTherapyRecords(query, "development") + const universalOrder = searchTherapyRecords(query) .slice(0, 5) .map(({ record }) => record.slug); @@ -87,6 +91,6 @@ describe("shared Therapy ranker", () => { ["DBT", "dialectical-behaviour-therapy-dbt"], ["EMDR", "eye-movement-desensitisation-and-reprocessing-emdr"], ])("ranks the exact %s alias first", (query, expectedSlug) => { - expect(searchTherapyRecords(query, "development")[0]?.record.slug).toBe(expectedSlug); + expect(searchTherapyRecords(query)[0]?.record.slug).toBe(expectedSlug); }); }); diff --git a/tests/therapy-review-regressions.test.ts b/tests/therapy-review-regressions.test.ts index d0c1fdc1b0..723d6f3992 100644 --- a/tests/therapy-review-regressions.test.ts +++ b/tests/therapy-review-regressions.test.ts @@ -29,16 +29,53 @@ describe("Therapy review regression contracts", () => { ); }); - it("keeps Therapy unavailable in production until clinical review is complete", () => { + // Replaces the former "keeps Therapy unavailable in production" contract. That + // gate hid the mode and 404'd all 205 records for real users; the owner's + // decision is to ship the library with its review state disclosed. These + // assertions pin the disclosure so the caveat cannot be dropped once the mode + // is reachable — the reachability half is pinned in tests/app-modes.test.ts. + it("keeps Therapy reachable with its review state disclosed instead of hidden", () => { const layout = source("src/app/(search-app)/therapy-compass/layout.tsx"); const modes = source("src/lib/app-modes.ts"); const therapies = source("src/lib/therapies.ts"); - expect(layout).toContain('isAppModeVisible("therapy-compass", "production")'); - expect(layout).toContain("notFound()"); - expect(modes).toMatch(/id: "therapy-compass"[\s\S]*?devOnly: true/); - expect(therapies).toContain('environment === "production"'); - expect(therapies).toContain("therapyNeedsReview(record)"); + // No environment gate may reappear on the route or the catalogue. + expect(layout).not.toContain("notFound()"); + expect(modes).not.toMatch(/id: "therapy-compass"[\s\S]*?devOnly: true/); + expect(therapies).not.toContain('environment === "production"'); + // Review status must survive as a label, not be deleted along with the gate. + expect(therapies).toContain("export function therapyNeedsReview"); + }); + + it("keeps the catalogue-wide review notice on the Therapy library, above the hero", () => { + const notice = source("src/components/therapy-compass/therapy-review-notice.tsx"); + const home = source("src/components/therapy-compass/screens/home-screen.tsx"); + + expect(notice).toContain('role="note"'); + expect(notice).toContain("THERAPY_CATALOGUE_SUMMARY.needsReviewCount"); + expect(notice).toContain("No therapy record in this library has completed clinician review yet."); + // Non-interactive: a caveat the reader can dismiss is not a caveat. + expect(notice).not.toContain(" { + for (const path of [ + "src/components/therapy-compass/therapy-card.tsx", + "src/components/therapy-compass/screens/detail-screen.tsx", + "src/components/therapy-compass/screens/brief-screen.tsx", + "src/components/therapy-compass/screens/sheets-screen.tsx", + "src/components/therapy-compass/screens/compare-screen.tsx", + "src/components/therapy-compass/screens/pathways-screen.tsx", + ]) { + expect(source(path), `${path} must still surface reviewStatus`).toContain("reviewStatus"); + } + // Discovery outside the mode carries it too, so an unreviewed therapy is + // flagged in universal search rather than only on its own page. + expect(source("src/lib/universal-search.ts")).toContain("Needs source review"); }); it("adds the favourites check without validating existing rows in the same migration", () => { From cad7dc117d632bba4d6a9ba85b525574d4d2b504 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Wed, 19 Aug 2026 02:56:14 +0800 Subject: [PATCH 2/2] docs(ledger): record the Therapy production-visibility review Co-Authored-By: Claude Opus 5 --- ...d1af3269ee1091efae428933fc910dc3da951590bdc62190344.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/730c1139a3494d1af3269ee1091efae428933fc910dc3da951590bdc62190344.record.md diff --git a/docs/branch-review-records/730c1139a3494d1af3269ee1091efae428933fc910dc3da951590bdc62190344.record.md b/docs/branch-review-records/730c1139a3494d1af3269ee1091efae428933fc910dc3da951590bdc62190344.record.md new file mode 100644 index 0000000000..be565add6d --- /dev/null +++ b/docs/branch-review-records/730c1139a3494d1af3269ee1091efae428933fc910dc3da951590bdc62190344.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/therapy-modes-visibility-bb37d2 | 9b4b3056b1f4ef7355e8947d6b210dee63de182e | Therapy production visibility: remove devOnly gate, route-layout not-found gate and production review filter; add catalogue review notice + needsReviewCount; retire PLAYWRIGHT_OFFLINE_MODE bypass; update pinning contracts | Approved — reachability now disclosed rather than gated; per-record reviewStatus badges retained on every surface; single-commit revert restores all three gates | verify:pr-local (docs/ledger/lint/typecheck passed; test failed only on unrelated load-flaky tests/codex-cloud-setup.test.ts, which passes in isolation at HEAD and with the change); build from wiped .next compiled successfully with all 9 therapy routes; check:rag:fixtures, check:medication-interactions, check:medication-lexicon-report passed; focused therapy+route-reachability contracts 77 passed; eslint+tsc clean; dev-server route 200s. verify:ui not run - coordinator heavy lock held by another worktree |