From ee440f41d0fe59366250fd0a02cddd1000c00471 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:06:52 +0000 Subject: [PATCH 1/3] docs(issues): defer Therapy sign-off tooling as an outstanding task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Queued at the owner's direction while Therapy is made production-visible ahead of clinical review. Three gaps it records: - `reviewStatus` is a bare string, so a record can be flipped to `reviewed` with all seven `reviewChecklist` booleans still false and nothing detects it. - No attribution: none of the 44 record fields carries `reviewedBy` or `reviewedAt`, the same defect `#318` flags against the medication interaction lexicon. - No review workflow: 205 records x 7 checks is 1435 clinical judgements by hand. Carries an explicit stop rule that an assistant must never tick the five clinical checks — those are qualified-clinician attestations — while `proofread` and `australianEnglishChecked` are non-clinical and may be done with attribution. Co-Authored-By: Claude Opus 5 --- .../1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/outstanding-issues-inbox/1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6.json diff --git a/docs/outstanding-issues-inbox/1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6.json b/docs/outstanding-issues-inbox/1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6.json new file mode 100644 index 0000000000..12790e3561 --- /dev/null +++ b/docs/outstanding-issues-inbox/1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "id": "1d0dbb76-a35d-4c8c-8816-3f82f3e1a5f6", + "createdOn": "2026-08-18", + "action": "add", + "payload": { + "pri": "P2", + "type": "task", + "summary": "Therapy sign-off tooling is deferred: no guard stops reviewStatus reviewed being set with an empty checklist, and there is no reviewer attribution", + "detail": "Deferred 2026-08-18 when Therapy was made production-visible ahead of clinical review. Three gaps. (1) reviewStatus is a bare string in src/data/therapies-source.json; a record can be flipped to reviewed with all seven reviewChecklist booleans still false and nothing detects it. Needs a script that refuses the flip unless the checklist is complete, plus a contract test pinning reviewed implies full checklist. (2) No attribution: none of the 44 record fields carries reviewedBy or reviewedAt, so a sign-off cannot record who signed or when - the same defect #318 flags against the medication interaction lexicon. (3) No review workflow: signing off 205 records x 7 checks is 1435 clinical judgements by hand; a CLI that walks records, shows the fields each check covers, and writes the decision with attribution would make it tractable. Current state at deferral: 205 records, all reviewStatus needs_review, all seven checklist booleans false, reviewCompleteness 57-71 with zero records complete. Stop rule: an assistant must never tick clinicalAccuracyReviewed, sourceChecked, evidenceAppraised, safetyCautionsChecked or patientExplanationChecked - those are qualified-clinician attestations. proofread and australianEnglishChecked are non-clinical and may be done with attribution.", + "issueUlid": "01M0ASAS3EJ2Z62XJV5ZQ8A08W" + } +} From 0e51be0ebbf22ec67efde77d6ca21b25c5f1fce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:06:52 +0000 Subject: [PATCH 2/3] feat(therapy): expose Therapy in production ahead of clinical sign-off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Therapy was gated out of production by two mechanisms: `devOnly: true` on the mode, which hid the entrance and made the generic route guard 404 the routes, and a record filter that served only `reviewStatus === "reviewed"`. With all 205 records `needs_review`, that meant an invisible mode over an empty library. The owner lifted both on 2026-08-18, deliberately and with the numbers in hand: 205 records, 0 reviewed, 1435 unticked checklist items. This is a private clinical reference tool and that exposure call is theirs. What was NOT done, because it would have been the easy way to the same screen: no record was relabelled. `reviewStatus` still reads `needs_review` on all 205. Marking them reviewed would have been a false clinical attestation rather than a product decision, and it would have destroyed the only signal a reader now has. The record filter is switched off by a named `HIDE_UNREVIEWED_IN_PRODUCTION` constant rather than deleted, so re-arming the gate after sign-off is a one-word edit, the condition stays greppable, and `therapyNeedsReview` keeps its meaning for every caller that surfaces review state. The route guard in `layout.tsx` is generic over `devOnly` and self-disarmed, so it is untouched and still protects the next mode that needs it. Three pinned tests are rewritten, not deleted, because the property they guarded moved rather than disappeared. While the gate stood, hiding the content WAS the protection. Now that it ships, the protection is that every surface still says "Needs source review" — so `therapy-ranking` pins exactly that, against the real catalogue: the record badge, the universal-search badge, the route metadata suffix, and the paired glyph that keeps the warning from being colour-only. `app-modes` inverts its visibility assertions so a silent re-hiding still fails, and `therapy-review-regressions` now asserts the gate machinery still exists and is re-armable rather than that it is engaged. Co-Authored-By: Claude Opus 5 --- src/lib/app-modes.ts | 17 ++++++-- src/lib/therapies.ts | 32 ++++++++++++++- tests/app-modes.test.ts | 20 ++++++--- tests/therapy-ranking.test.ts | 52 +++++++++++++++++++++--- tests/therapy-review-regressions.test.ts | 25 +++++++++++- 5 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/lib/app-modes.ts b/src/lib/app-modes.ts index 00193ba8ae..e028a826f6 100644 --- a/src/lib/app-modes.ts +++ b/src/lib/app-modes.ts @@ -368,10 +368,19 @@ 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 was production-gated while its catalogue awaited qualified-clinician + // sign-off. The owner lifted that gate on 2026-08-18 with the catalogue still + // unreviewed — a deliberate, informed decision for a private clinical + // reference tool, taken knowing all 205 records are `needs_review`. + // + // The records were NOT relabelled to buy this: `reviewStatus` still reads + // `needs_review`, and the per-record signals that say so — the "Needs source + // review" search badge, the "(Awaiting source review.)" metadata suffix, and + // `StatusBadge` on the record — are now the only thing standing between an + // unreviewed record and a clinical decision. `tests/therapy-ranking.test.ts` + // pins them, and that pin replaced the old "hidden in production" assertion + // rather than deleting it. See `HIDE_UNREVIEWED_IN_PRODUCTION` in + // `src/lib/therapies.ts` for the matching record-level switch. 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..fb4b9ad7d3 100644 --- a/src/lib/therapies.ts +++ b/src/lib/therapies.ts @@ -32,9 +32,34 @@ function defaultTherapyEnvironment(): string | undefined { return process.env.PLAYWRIGHT_OFFLINE_MODE === "true" ? "development" : process.env.NODE_ENV; } +/** + * Whether production withholds therapy records that have not completed clinical + * review. + * + * Was implicitly `true`: production filtered every record whose `reviewStatus` + * was not `reviewed`, which — with the whole catalogue unreviewed — meant an + * empty library, and `devOnly` on the mode hid the entrance entirely. The owner + * lifted both on 2026-08-18, deliberately and with the numbers in hand: 205 + * records, 0 reviewed, 1435 unticked checklist items. + * + * Written as a named constant rather than deleting the filter, for three + * reasons. Re-arming the gate is a one-word edit if that decision is revisited; + * the condition stays greppable, so "why is unreviewed content live?" has an + * answer at the point of the decision; and `therapyNeedsReview` keeps its + * meaning for every caller that still surfaces review state to the reader — + * which, now that this filter is off, is the only remaining protection. + * + * Nothing here changes a record's data. `reviewStatus` is still `needs_review`, + * because the alternative — relabelling records as reviewed — would have been a + * false clinical attestation rather than a product decision. + */ +const HIDE_UNREVIEWED_IN_PRODUCTION = false; + /** 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; + return HIDE_UNREVIEWED_IN_PRODUCTION && environment === "production" + ? therapyRecords.filter((record) => !therapyNeedsReview(record)) + : therapyRecords; } export function findTherapyRecord( @@ -42,7 +67,10 @@ export function findTherapyRecord( environment = defaultTherapyEnvironment(), ): TherapyIndexRecord | undefined { const record = bySlug.get(slug); - return record && (environment !== "production" || !therapyNeedsReview(record)) ? record : undefined; + if (!record) return undefined; + return !HIDE_UNREVIEWED_IN_PRODUCTION || environment !== "production" || !therapyNeedsReview(record) + ? record + : undefined; } export function therapyRecordExists(slug: string, environment = defaultTherapyEnvironment()): boolean { diff --git a/tests/app-modes.test.ts b/tests/app-modes.test.ts index a4ae603758..1f95f840ea 100644 --- a/tests/app-modes.test.ts +++ b/tests/app-modes.test.ts @@ -316,7 +316,10 @@ 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); + // Therapy joined the production modes on 2026-08-18 when the owner lifted its + // clinical-review gate. Asserted `true` rather than dropped, so a silent + // re-hiding still fails a test. + expect(isAppModeVisible("therapy-compass", "production")).toBe(true); expect(isAppModeVisible("factsheets", "production")).toBe(true); expect(productionModes).not.toContain("evidence"); expect(productionModes).toContain("services"); @@ -329,7 +332,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 +355,19 @@ describe("app mode search contract", () => { expect(developmentModes).not.toContain("evidence"); }); - it("keeps Therapy Compass behind clinical review in production", () => { + // Was "keeps Therapy Compass behind clinical review in production". The owner + // lifted that gate on 2026-08-18 with the catalogue still unreviewed, so the + // old assertion is untrue rather than merely unenforced. Inverted rather than + // removed: Therapy is now an ordinary production mode in both environments, + // and an accidental return of `devOnly` fails here. The clinical-review + // property it used to protect did not vanish — it moved to the per-record + // signals pinned in `tests/therapy-ranking.test.ts`. + it("exposes Therapy Compass as an ordinary production mode", () => { 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-ranking.test.ts b/tests/therapy-ranking.test.ts index 5b7d0ff215..5bb68e73b2 100644 --- a/tests/therapy-ranking.test.ts +++ b/tests/therapy-ranking.test.ts @@ -1,13 +1,14 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { EMPTY_SEARCH, searchTherapies } from "@/components/therapy-compass/data/select"; +import { EMPTY_SEARCH, reviewStatusMeta, searchTherapies } from "@/components/therapy-compass/data/select"; import { THERAPY_CATALOGUE_ASSETS } from "@/components/therapy-compass/data/generated-assets"; import type { Therapy } from "@/components/therapy-compass/data/types"; import { rankTherapyCandidates, scoreTherapyCandidate } from "@/lib/therapy-ranking"; import { findTherapyRecord, searchTherapyRecords, + therapyNeedsReview, therapyRecords, therapyRecordsForEnvironment, therapySlugs, @@ -50,12 +51,51 @@ 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", () => { + /** + * Was "excludes unreviewed Therapy content from production discovery and + * routes". That assertion is now false by decision, not by accident: the owner + * lifted the production review gate on 2026-08-18 with all 205 records still + * `needs_review`, so production serves them. + * + * Replaced rather than deleted, and with the stronger requirement. While the + * gate stood, hiding the content WAS the protection and nothing needed to say + * "unreviewed" out loud. Now that the content ships, the only thing between an + * unreviewed record and a clinical decision is that every surface still says + * so — so that is what gets pinned, on the real catalogue rather than fixtures. + * + * If someone re-arms `HIDE_UNREVIEWED_IN_PRODUCTION`, the first three + * assertions fail loudly and this comment is where they should look. + */ + it("serves the catalogue in production and never drops the unreviewed marking", () => { 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(); + expect(therapyRecordsForEnvironment("production")).toHaveLength(therapyRecords.length); + expect(therapySlugs("production")).toHaveLength(therapyRecords.length); + expect(findTherapyRecord(therapyRecords[0].slug, "production")).toBeDefined(); + expect(searchTherapyRecords("CBT", "production").length).toBeGreaterThan(0); + + // The marking itself. `reviewStatus` must stay honest — relabelling records + // as reviewed would be a false clinical attestation, not a product change. + const unreviewed = therapyRecords.filter((record) => record.reviewStatus !== "reviewed"); + expect(unreviewed.length).toBeGreaterThan(0); + expect(therapyNeedsReview(unreviewed[0])).toBe(true); + + // Every reader-facing channel that carries it, so none can be dropped + // silently: the record badge, the universal-search badge, and the route + // metadata description. + expect(reviewStatusMeta(unreviewed[0].reviewStatus)).toEqual({ + label: "Needs source review", + tone: "warning", + }); + expect(readFileSync("src/lib/universal-search.ts", "utf8")).toContain( + 'badge: therapyNeedsReview(record) ? "Needs source review"', + ); + expect(readFileSync("src/app/(search-app)/therapy-compass/[slug]/page.tsx", "utf8")).toContain( + "Awaiting source review.", + ); + // Tone alone is not a channel — `StatusBadge` pairs it with a glyph. + expect(readFileSync("src/components/therapy-compass/ui.tsx", "utf8")).toContain( + 'const Icon = meta.tone === "success" ? ShieldCheck : TriangleAlert;', + ); }); it.each([ diff --git a/tests/therapy-review-regressions.test.ts b/tests/therapy-review-regressions.test.ts index d0c1fdc1b0..f9b07792bd 100644 --- a/tests/therapy-review-regressions.test.ts +++ b/tests/therapy-review-regressions.test.ts @@ -29,14 +29,35 @@ describe("Therapy review regression contracts", () => { ); }); - it("keeps Therapy unavailable in production until clinical review is complete", () => { + /** + * Was "keeps Therapy unavailable in production until clinical review is + * complete". The owner lifted that gate on 2026-08-18 with the catalogue still + * unreviewed, so the old name now describes a state the product deliberately + * does not hold. + * + * The machinery is asserted to still EXIST rather than being torn out, which + * is the point of keeping this test. `devOnly` was removed from the mode and + * `HIDE_UNREVIEWED_IN_PRODUCTION` is `false`, but the generic route guard, + * the record filter and `therapyNeedsReview` all remain wired — so re-arming + * the gate after sign-off is a two-line edit, not a rebuild. A future change + * that deletes the mechanism outright fails here. + */ + it("keeps the Therapy review gate re-armable after its deliberate lifting", () => { 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"); + // The route guard is generic over `devOnly`, so it self-disarmed when the + // flag came off and did not need editing. It must stay for the next mode. expect(layout).toContain('isAppModeVisible("therapy-compass", "production")'); expect(layout).toContain("notFound()"); - expect(modes).toMatch(/id: "therapy-compass"[\s\S]*?devOnly: true/); + + // Therapy is no longer dev-only, and the reason is recorded at the site. + expect(modes).not.toMatch(/id: "therapy-compass"[\s\S]{0,600}?devOnly: true/); + expect(modes).toContain("HIDE_UNREVIEWED_IN_PRODUCTION"); + + // The record-level filter survives, switched off by one named constant. + expect(therapies).toContain("const HIDE_UNREVIEWED_IN_PRODUCTION = false;"); expect(therapies).toContain('environment === "production"'); expect(therapies).toContain("therapyNeedsReview(record)"); }); From 5c7e4d6c951f8187516d82b88f5f6679c1340450 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 17:54:35 +0000 Subject: [PATCH 3/3] docs(ledger): record the Therapy production-exposure review for PR #2145 Immutable record for the owner-directed lifting of the Therapy clinical review gate, including the numbers the decision was taken on and the explicit note that no record was relabelled. Co-Authored-By: Claude Opus 5 --- ...84e008441a428df84d3a6f127b81692ab314e8bf97a54197ffd.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/60f7559f21cf484e008441a428df84d3a6f127b81692ab314e8bf97a54197ffd.record.md diff --git a/docs/branch-review-records/60f7559f21cf484e008441a428df84d3a6f127b81692ab314e8bf97a54197ffd.record.md b/docs/branch-review-records/60f7559f21cf484e008441a428df84d3a6f127b81692ab314e8bf97a54197ffd.record.md new file mode 100644 index 0000000000..39644cfec7 --- /dev/null +++ b/docs/branch-review-records/60f7559f21cf484e008441a428df84d3a6f127b81692ab314e8bf97a54197ffd.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/therapy-production-visibility | 0e51be0ebbf22ec67efde77d6ca21b25c5f1fce9 | Therapy production exposure: remove devOnly, switch off the unreviewed-record filter behind a named constant, rewrite three pinned gate tests | Approved for review as PR #2145. Owner-directed exposure of the Therapy catalogue ahead of clinical sign-off, taken with the numbers stated: 205 records, 0 reviewed, 1435 unticked checklist items. No record was relabelled - reviewStatus stays needs_review on all 205, because marking them reviewed would be a false clinical attestation. Gate machinery retained and re-armable via HIDE_UNREVIEWED_IN_PRODUCTION; layout route guard untouched as it self-disarms. Three pinned tests rewritten to the stronger post-gate condition (honesty channels pinned against the real catalogue, including the non-colour-only glyph) rather than deleted. pr-policy returns clinicalRisk false for this diff, which is a classifier gap flagged in the PR body; governance preflight completed voluntarily with the review-status conservatism item deliberately left unticked as it would be untrue. | verify:cheap green - Test Files 673 passed (673), Tests 7276 passed \| 4 skipped (7280), Design-system contract passed (784 production files); verify:ui 447 passed (14.9m) chromium on the Playwright-matched revision; check:production-readiness ran with 2 FAIL, both missing Supabase/OpenAI env in the offline container (documented provider capability gap, unrelated to this diff) |