Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ scratch/
src/lib/supabase/database.types.ts
supabase/drift-manifest.json
# Trimmed Therapy Compass content export (generated data assets); keep minified.
public/mockups/therapy-compass/
public/therapy-compass-data/
2 changes: 2 additions & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,3 +553,5 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-14 | cursor/fix-pr654-ci-53b4 | 9f880853ea7d268186d982f4623b71f46e77d3dc | branch-cleanup-deletion-pending | Pending deletion: content already on origin/main or superseded old-lineage snapshot / throwaway CI-retry branch; branch-only-file sweep found no novel work. Remote deletion DENIED (HTTP 403, session lacks ref-delete permission); branch still exists at this HEAD — operator must complete deletion. Scope is deletion-pending so future cleanup passes re-evaluate rather than skip. | Provider-backed GitHub PR inventory via MCP (400 PRs #264-#674, all merged=false); local read-only git: rewritten-main-aware cherry-pick + two-dot tree + branch-only-file novelty sweep vs origin/main e75fad90; adversarial re-check. Remote `git push --delete` attempted (provider-backed write) and DENIED with HTTP 403. No OpenAI/Supabase/live-eval checks. |
| 2026-07-14 | fix/accessibility-remaining-findings | 9f880853ea7d268186d982f4623b71f46e77d3dc | branch-cleanup-deletion-pending | Pending deletion: content already on origin/main or superseded old-lineage snapshot / throwaway CI-retry branch; branch-only-file sweep found no novel work. Remote deletion DENIED (HTTP 403, session lacks ref-delete permission); branch still exists at this HEAD — operator must complete deletion. Scope is deletion-pending so future cleanup passes re-evaluate rather than skip. | Provider-backed GitHub PR inventory via MCP (400 PRs #264-#674, all merged=false); local read-only git: rewritten-main-aware cherry-pick + two-dot tree + branch-only-file novelty sweep vs origin/main e75fad90; adversarial re-check. Remote `git push --delete` attempted (provider-backed write) and DENIED with HTTP 403. No OpenAI/Supabase/live-eval checks. |
| 2026-07-15 | PR #677 / claude/cleanup-branches-worktrees-mxov4x | a93db73a29ca6a19619a6592e2e30ca9ea2f8218 | active PR review and remediation | All three actionable review findings are fixed: every branch namespace is parsed from the ledger table, only an exact completed cleanup review at the current HEAD suppresses repeat work, pending deletions remain actionable, and GitHub provider provenance is accurate. No additional high-confidence defect remains in the changed scope. | GitHub unresolved-thread inventory (0 remaining); hosted required CI green; exact-head focused Vitest `tests/repo-hygiene.test.ts` (9/9); `node scripts/sweep-branch-ledger.mjs --no-fetch --json`; `git diff --check`. No Supabase, OpenAI, or other live-service checks. |
| 2026-07-15 | PR #656 / claude/specifiers-v2-design-r55baf | 58ce935758c31672a0a051c5b3e6b888a7d8d153 | full DSM/ICD specifier catalogue and clinical-content gate review | No remaining high-confidence code defect after the review sequence corrected source provenance, verified-content wording, search ranking/deduplication, empty-state behavior, and neutral mixed-source labelling. The 494 unverified definitions remain withheld from display and ranking. Residual risk is the PR-declared qualified-clinician and TGA classification review before broader clinical deployment. | GitHub review-thread inventory (0 unresolved); exact-head hosted required CI, build, critical UI, UI regression, coverage, static, and security checks green. No live Supabase/OpenAI or provider-backed clinical workflow run. |
| 2026-07-15 | PR #679 / claude/therapy-compass-pages-rz0m5l | f5ca25f8b6b14f41e63f708933fe4bb311994795 | Therapy Compass production promotion and review-followup | All five review findings are fixed on the reviewed head: run-enabled shared-composer links stay on the Therapy Compass route, production data loads outside `/mockups`, deep links seed the in-tool search, and same-route query changes remount the provider. No additional high-confidence defect remains in the changed scope. | GitHub review-thread inventory; exact-head hosted build, critical UI smoke, UI regression, unit coverage, static, security, and image checks green; focused Vitest 28/28; TypeScript; `git diff --check`. No live Supabase/OpenAI checks run. |
2 changes: 2 additions & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/specifiers/builder` - Structured diagnostic wording builder. Source: `src/app/specifiers/builder/page.tsx`.
- `/specifiers/compare` - Side-by-side psychiatric specifier comparison. Source: `src/app/specifiers/compare/page.tsx`.
- `/specifiers/map` - Psychiatric specifier family map. Source: `src/app/specifiers/map/page.tsx`.
- `/therapy-compass` - Route discovered from app directory Source: `src/app/therapy-compass/page.tsx`.
- `/tools` - Route discovered from app directory Source: `src/app/tools/page.tsx`.

## Mode/query routes
Expand All @@ -45,6 +46,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/formulation` - Formulation mode. Search kind: `formulation`. Query example: `/formulation?q=I+keep+going+over+it&focus=1&run=1`.
- `/?mode=prescribing` - Medication mode. Search kind: `documents`. Query example: `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1`.
- `/?mode=tools` - Tools mode. Search kind: `tools`. Query example: `/?mode=tools&q=medications&focus=1&run=1`.
- `/therapy-compass` - Therapy Compass mode. Search kind: `tools`. Query example: `/therapy-compass?q=behavioural+activation&focus=1&run=1`.

## Mode page index

Expand Down
1 change: 1 addition & 0 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ function renderModeRoutes() {
formulation: appModeHomeHref("formulation", { query: "I keep going over it", focus: true, run: true }),
prescribing: appModeHomeHref("prescribing", { query: "acamprosate renal dose", focus: true, run: true }),
tools: appModeHomeHref("tools", { query: "medications", focus: true, run: true }),
"therapy-compass": appModeHomeHref("therapy-compass", { query: "behavioural activation", focus: true, run: true }),
};

return appModeDefinitions.map((mode) =>
Expand Down
14 changes: 14 additions & 0 deletions src/app/therapy-compass/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";

import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell";

// Therapy Compass keeps the universal header + rail but provides its own primary
// search surface (the in-tool therapy search), so the shared search composer is
// suppressed here — mirroring how the mockup mounted it.
export default function TherapyCompassLayout({ children }: { children: ReactNode }) {
return (
<GlobalSearchShell initialMode="therapy-compass" searchComposerVisible={false}>
{children}
</GlobalSearchShell>
);
}
24 changes: 24 additions & 0 deletions src/app/therapy-compass/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Metadata } from "next";

import { TherapyCompassPage } from "@/components/therapy-compass";

export const metadata: Metadata = {
title: "Therapy Compass - Clinical KB",
description:
"Source-grounded therapy decision support: search, compare, recommend, pathways, brief interventions and patient sheets.",
};

type TherapyCompassRouteProps = {
searchParams?: Promise<{ q?: string | string[]; run?: string | string[] }>;
};

function firstParam(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value;
}

export default async function TherapyCompassRoute({ searchParams }: TherapyCompassRouteProps) {
const params = searchParams ? await searchParams : {};
const query = firstParam(params.q)?.trim() ?? "";
const autoRunSearch = firstParam(params.run) === "1" && query.length > 0;
return <TherapyCompassPage initialQuery={query} autoRunSearch={autoRunSearch} />;
}
1 change: 1 addition & 0 deletions src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const sidebarToolItems = [
{ id: "formulation", label: "Formulation", icon: Network, href: "/formulation" },
{ id: "prescribing", label: "Medication", icon: Pill, href: "/?mode=prescribing" },
{ id: "tools", label: "Tools", icon: Wrench, href: "/?mode=tools" },
{ id: "therapy-compass", label: "Therapy Compass", icon: appModeIcons["therapy-compass"], href: "/therapy-compass" },
] as const;

function sidebarItemBadge(item: (typeof sidebarToolItems)[number]): string | undefined {
Expand Down
6 changes: 6 additions & 0 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ function GlobalSearchShellClient(props: GlobalSearchShellProps) {
resolvedSearchMode !== "dsm" &&
resolvedSearchMode !== "specifiers" &&
resolvedSearchMode !== "formulation" &&
// Therapy Compass owns its route with an in-tool search; a run-enabled link
// (/therapy-compass?q=…&run=1) must keep rendering the tool, not the dashboard.
resolvedSearchMode !== "therapy-compass" &&
!isDocumentSearchMockupRoute;
const isMedicationDetailRoute = /^\/medications\/[^/]+$/.test(pathname);
const shouldRenderClinicalDashboard = !isMedicationDetailRoute && (isHomeRoute || shouldRenderDashboardSearch);
Expand Down Expand Up @@ -251,6 +254,9 @@ function GlobalStandaloneSearchShellClient({
resolvedSearchMode !== "dsm" &&
resolvedSearchMode !== "specifiers" &&
resolvedSearchMode !== "formulation" &&
// Therapy Compass owns its route with an in-tool search; a run-enabled link
// (/therapy-compass?q=…&run=1) must keep rendering the tool, not the dashboard.
resolvedSearchMode !== "therapy-compass" &&
!isDocumentSearchMockupRoute;
const isStandaloneModeHome =
!hasSubmittedModeSearch &&
Expand Down
22 changes: 19 additions & 3 deletions src/components/therapy-compass/bindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,31 @@ function chipStyle(on: boolean): CSSProperties {
);
}

export function TcProvider({ children }: { children: ReactNode }) {
export function TcProvider({
children,
initialQuery = "",
autoRunSearch = false,
}: {
children: ReactNode;
initialQuery?: string;
autoRunSearch?: boolean;
}) {
const { data, loading, error } = useTherapyData();
const therapies = useMemo(() => data?.therapies ?? [], [data]);
const pathways = useMemo(() => data?.pathways ?? [], [data]);

const [screen, setScreen] = useState<string>("home");
// Honor a run-enabled deep link (/therapy-compass?q=…&run=1): open on Search
// with the query seeded, so a query submitted from the universal composer or a
// recent-search pick runs in-tool instead of landing on Home. A fresh deep link
// while already mounted re-seeds via the provider key in TherapyCompassPage,
// which remounts this provider when the run-query changes.
const seededQuery = autoRunSearch ? initialQuery.trim() : "";
const [screen, setScreen] = useState<string>(seededQuery ? "search" : "home");
const [selectedSlug, setSelectedSlug] = useState<string | null>(null);
const [compareSlugs, setCompareSlugs] = useState<string[]>([]);
const [search, setSearch] = useState<SearchOptions>(EMPTY_SEARCH);
const [search, setSearch] = useState<SearchOptions>(
seededQuery ? { ...EMPTY_SEARCH, query: seededQuery } : EMPTY_SEARCH,
);
Comment thread
BigSimmo marked this conversation as resolved.
const [recQuery, setRecQuery] = useState("What therapy for anxiety in outpatient care?");
const [recConstraints, setRecConstraints] = useState<string[]>(["outpatient"]);
const [selectedPathwaySlug, setSelectedPathwaySlug] = useState<string | null>(null);
Expand Down
10 changes: 6 additions & 4 deletions src/components/therapy-compass/data/use-therapy-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { useEffect, useState } from "react";

import type { Pathway, ReferenceData, Therapy, TherapyDataset } from "./types";

const BASE = "/mockups/therapy-compass";
// Served as static public assets. Kept outside /mockups so the production
// `/therapy-compass` route can load them — `proxy.ts` 404s every /mockups path
// in production, which would otherwise starve the tool of its dataset.
const BASE = "/therapy-compass-data";

// Module-level cache so the ~2.7 MB dataset is fetched at most once per session,
// shared across every screen. The route is dev-only, so these static assets
// never ship to production.
// Module-level cache so the ~2.6 MB dataset is fetched at most once per session,
// shared across every screen.
let cache: Promise<TherapyDataset> | null = null;

async function fetchJson<T>(path: string): Promise<T> {
Expand Down
14 changes: 12 additions & 2 deletions src/components/therapy-compass/therapy-compass-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,19 @@ function TherapyCompassShell() {
* design's own left rail is dropped, with its destinations kept reachable via a
* horizontal in-content nav under the global header.
*/
export function TherapyCompassPage() {
export function TherapyCompassPage({
initialQuery = "",
autoRunSearch = false,
}: {
initialQuery?: string;
autoRunSearch?: boolean;
}) {
// Remount the provider when a fresh run-enabled deep link arrives so its seed
// re-runs — the App Router preserves client state across same-route navigations,
// so without this a new /therapy-compass?q=…&run=1 would keep the prior search.
const seedKey = autoRunSearch && initialQuery.trim() ? `q:${initialQuery.trim()}` : "home";
return (
<TcProvider>
<TcProvider key={seedKey} initialQuery={initialQuery} autoRunSearch={autoRunSearch}>
<TherapyCompassStyles />
<TherapyCompassShell />
</TcProvider>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/app-mode-icons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BookOpenCheck,
BrainCircuit,
Compass,
FileSignature,
FileText,
Heart,
Expand Down Expand Up @@ -28,4 +29,5 @@ export const appModeIcons: Record<AppModeId, LucideIcon> = {
formulation: Network,
prescribing: Pill,
tools: Wrench,
"therapy-compass": Compass,
};
27 changes: 27 additions & 0 deletions src/lib/app-modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const appModeIds = [
"formulation",
"prescribing",
"tools",
"therapy-compass",
] as const;

export type AppModeId = (typeof appModeIds)[number];
Expand Down Expand Up @@ -307,6 +308,31 @@ export const appModeDefinitions = [
badgeLabel: null,
},
},
{
id: "therapy-compass",
label: "Therapy Compass",
description: "Source-grounded therapy decision support",
href: "/therapy-compass",
search: {
// Therapy Compass owns its in-tool search over the imported therapy library
// (not the document corpus), so it borrows the benign "tools" search kind and
// suppresses the universal composer on its route.
kind: "tools",
placeholder: "Search therapies, symptoms, or skills...",
inputAriaLabel: "Search therapies by problem, symptom, skill, or population",
submitIdleLabel: "Compass",
submitBusyLabel: "Compass",
submitAriaLabel: "Open Therapy Compass",
emptyTitle: "Browse the therapy library",
readyTitle: "Search source-grounded therapies",
progressLabel: "Loading the therapy library.",
resultKind: "tools",
resultHeading: "Therapies",
statusLabel: "Compass",
nextStep: "Open a therapy record",
badgeLabel: null,
},
},
] as const satisfies readonly AppModeDefinition[];

export function appModeDefinition(modeId: AppModeId) {
Expand Down Expand Up @@ -339,6 +365,7 @@ const namespaceIsolatedModes = new Set<AppModeId>([
"dsm",
"specifiers",
"formulation",
"therapy-compass",
Comment thread
BigSimmo marked this conversation as resolved.
Comment thread
BigSimmo marked this conversation as resolved.
Comment thread
BigSimmo marked this conversation as resolved.
]);

export function appModeHomeHref(modeId: AppModeId, options: SearchNavigationOptions = {}) {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/universal-search-mode-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const preferredDomainsByMode: Record<AppModeId, readonly UniversalSearchDomain[]
formulation: ["formulation"],
prescribing: ["medications", "documents"],
tools: ["tools"],
// Therapy Compass searches the imported therapy library in-tool, not an indexed
// universal-search domain, so it contributes no cross-entity domains.
"therapy-compass": [],
};

const modeByDomain: Record<UniversalSearchDomain, AppModeId> = {
Expand Down
50 changes: 50 additions & 0 deletions tests/therapy-compass-mode-wiring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { existsSync, readFileSync } from "node:fs";

import { describe, expect, it } from "vitest";

// Guards the two production-mode wiring invariants for Therapy Compass. Both were
// real breakages caught in review when the mockup was promoted to a live mode.

const loaderSrc = readFileSync(
new URL("../src/components/therapy-compass/data/use-therapy-data.ts", import.meta.url),
"utf8",
);
const shellSrc = readFileSync(
new URL("../src/components/clinical-dashboard/global-search-shell.tsx", import.meta.url),
"utf8",
);
const dataDir = new URL("../public/therapy-compass-data/", import.meta.url);

describe("Therapy Compass production-mode wiring", () => {
it("loads its dataset from a non-/mockups path (proxy.ts 404s every /mockups path in production)", () => {
const base = loaderSrc.match(/const BASE = "([^"]+)"/)?.[1];
expect(base).toBeTruthy();
expect(base).not.toMatch(/^\/mockups/);
});

it("ships the dataset at the non-mockups public path the loader points to", () => {
for (const file of ["therapies.json", "pathways.json", "reference.json"]) {
expect(existsSync(new URL(file, dataDir))).toBe(true);
}
});

it("excludes therapy-compass from the shell dashboard-search so run-enabled links keep the tool", () => {
// Both shouldRenderDashboardSearch blocks (standalone + non-standalone client) must exclude the mode,
// otherwise /therapy-compass?q=…&run=1 renders ClinicalDashboard over TherapyCompassPage.
const occurrences = shellSrc.match(/resolvedSearchMode !== "therapy-compass"/g) ?? [];
expect(occurrences.length).toBeGreaterThanOrEqual(2);
});

it("honors run-enabled deep links by seeding the in-tool search instead of landing on Home", () => {
const routeSrc = readFileSync(new URL("../src/app/therapy-compass/page.tsx", import.meta.url), "utf8");
const bindingsSrc = readFileSync(
new URL("../src/components/therapy-compass/bindings.tsx", import.meta.url),
"utf8",
);
// The route reads q/run and threads autoRunSearch into the provider...
expect(routeSrc).toMatch(/searchParams/);
expect(routeSrc).toMatch(/autoRunSearch/);
// ...and the provider opens on Search (not Home) with the query seeded.
expect(bindingsSrc).toMatch(/seededQuery \? "search" : "home"/);
});
});
43 changes: 43 additions & 0 deletions tests/therapy-compass-provider-seed.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { TcProvider, useTcBindings } from "@/components/therapy-compass/bindings";

// TcProvider fetches its dataset on mount; stub fetch so it mounts without
// network. The screen/search seed is independent of data loading, which is what
// this test exercises.
afterEach(() => vi.unstubAllGlobals());

function Probe() {
const b = useTcBindings();
return <div data-testid="probe" data-screen={b.screen} data-query={b.search.query} />;
}

describe("TcProvider run-enabled seeding", () => {
it("seeds Search + query on mount and re-seeds when the deep-link query changes", () => {
// Never-resolving fetch → the provider stays in its loading state without needing data.
vi.stubGlobal(
"fetch",
vi.fn(() => new Promise(() => {})),
);

const { getByTestId, rerender } = render(
<TcProvider key="q:alpha" initialQuery="alpha" autoRunSearch>
<Probe />
</TcProvider>,
);
expect(getByTestId("probe").getAttribute("data-screen")).toBe("search");
expect(getByTestId("probe").getAttribute("data-query")).toBe("alpha");

// A fresh /therapy-compass?q=beta&run=1 navigation changes the provider key
// (as TherapyCompassPage computes it), remounting the provider so its seed
// re-runs for the new query.
rerender(
<TcProvider key="q:beta" initialQuery="beta" autoRunSearch>
<Probe />
</TcProvider>,
);
expect(getByTestId("probe").getAttribute("data-screen")).toBe("search");
expect(getByTestId("probe").getAttribute("data-query")).toBe("beta");
});
});