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
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,3 +490,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-14 | PR #655 / codex/release-blocker-remediation | 3ed3a7a2df37d7d15143ab7606e5748ac7ecca09 + reviewed follow-up diff | offline dose-route latency follow-up | The next isolated timeout was a short IM/PO agitation question receiving the same blanket ten-term AND expansion. Agitation dose/route retrieval now keeps only the dose and route signals present in the question; the exact case retrieves its expected source and five citations in 1.54 seconds without a model. All remaining RAG cases 23–44 passed individually, so no further deadline crash remains. | Focused clinical-search/retrieval Vitest 111/111; scoped ESLint; Prettier; `git diff --check`; live provider-free case 22 passed; live provider-free cases 23–44 passed individually. |
| 2026-07-14 | PR #655 / codex/release-blocker-remediation | a3f3a89676015cd5f018c07e8c3ad9483f91cef6 + reviewed follow-up diff | offline adversarial-latency follow-up | The final blocking offline-quality failure was an adversarial secret-exfiltration query that correctly refused but first spent about 25 seconds in lexical retrieval. Adversarial manipulation now short-circuits at the search boundary before provider-client creation, cache access, classification, aliases, or Supabase work, and is never cached. | Focused Vitest 2/2; scoped ESLint; Prettier; full TypeScript; `git diff --check`; live provider-free adversarial case completed in 100 ms with 0 ms RPC time; `eval:quality:release:offline` passed with zero blocking failures and zero model, request-ID, token, cost, or generation-latency evidence. Flaky local browser/composite suites intentionally not repeated; hosted CI remains authoritative. |
| 2026-07-14 | PR #666 / codex/release-blocker-remediation | 9b56eebe4b23ab783207445fb827c317c8d59be8 + reviewed follow-up diff | review-followup | One late P2 retrieval-contract gap was confirmed: the optimized agitation query retained IM/PO but could drop other already-supported amount, route, and frequency aliases. Medication evidence intent is now shared with retrieval selection, and focused agitation queries preserve requested numeric units, SC, SL, PRN, and frequency signals without restoring the broad ten-term expansion. | GitHub review-thread inspection; focused clinical-search/retrieval Vitest 112/112; scoped ESLint; Prettier; `git diff --check`. Hosted final-head TypeScript/build/CI and exact-head staging evidence remain required after push. |
| 2026-07-14 | codex/formulation-specifier-separation-current | 25d7f733f + uncommitted fix | Specifiers/Formulation boundary bug hunt and remediation | Confirmed two P1 clinical-navigation/content boundary defects: the completed Formulation change deleted the distinct psychiatric Specifiers workspace and redirected every `/specifiers/*` route into Formulation; a later universal-search contract also labelled Formulation mechanism results as `specifiers`. Restored the independent Specifiers catalogue, detail, builder, compare, and map surfaces; registered both app modes and action sets; kept submitted searches in their owning standalone workspace; and split universal search into real `specifiers` and `formulation` domains with correct records, headings, mode ownership, and destinations. | Offline only: full Vitest 2,404 passed/1 skipped, TypeScript, full ESLint, focused Vitest 56/56, sitemap check, `git diff --check`, production-readiness READY with expected missing-env warnings, identity-checked server at `http://localhost:3384`, and repaired Chromium journeys for Specifiers desktop/mobile, Formulation mobile, and explicit route-family separation all passed. No provider-backed or live API checks run. |
25 changes: 22 additions & 3 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/privacy` - Privacy and data-processing governance draft. Source: `src/app/privacy/page.tsx`.
- `/reference/colour-coding` - Route discovered from app directory Source: `src/app/reference/colour-coding/page.tsx`.
- `/services` - Services home and search surface. Source: `src/app/services/page.tsx`.
- `/specifiers/[[...path]]` - Compatibility redirect to the corrected Formulation workspace. Source: `src/app/specifiers/[[...path]]/page.tsx`.
- `/specifiers` - Psychiatric specifier home and local search surface. Source: `src/app/specifiers/page.tsx`.
- `/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`.
- `/tools` - Route discovered from app directory Source: `src/app/tools/page.tsx`.

## Mode/query routes
Expand Down Expand Up @@ -54,7 +57,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Favourites | `/favourites` | `/favourites?q=clozapine+set&focus=1&run=1` | Saved set and saved item detail render inside the favourites page surface. |
| Differentials | `/differentials` | `/differentials?q=acute+confusion&focus=1&run=1` | `/differentials/diagnoses`, `/differentials/diagnoses/[slug]`, and `/differentials/presentations`. |
| DSM-5 Diagnosis | `/dsm` | `/dsm/search?q=major+depressive+disorder&focus=1&run=1` | `/dsm/diagnoses/[slug]`, `/dsm/compare`, and `/dsm/diagnoses/[slug]/differentials`. |
| Specifiers | `/specifiers` | `/specifiers?q=depressed+but+racing+thoughts&focus=1&run=1` | Compatibility entry that redirects through `/specifiers` into the Formulation workspace. |
| Specifiers | `/specifiers` | `/specifiers?q=depressed+but+racing+thoughts&focus=1&run=1` | `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, and `/specifiers/map`. |
| Formulation | `/formulation` | `/formulation?q=I+keep+going+over+it&focus=1&run=1` | `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, and `/formulation/map`. |
| Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). |
Expand Down Expand Up @@ -300,6 +303,22 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `youthreach-south`
- `youthreach-south-ycatt`

### Psychiatric specifier slugs

- `/specifiers/[slug]` - Dynamic route family.
- `in-full-remission`
- `in-partial-remission`
- `mild-severity`
- `with-anxious-distress`
- `with-atypical-features`
- `with-catatonia`
- `with-melancholic-features`
- `with-mixed-features`
- `with-peripartum-onset`
- `with-psychotic-features`
- `with-rapid-cycling`
- `with-seasonal-pattern`

### Seeded form slugs

- `/forms/[slug]` - Dynamic route family.
Expand Down Expand Up @@ -791,7 +810,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Favourites | `src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx` |
| Differentials | `src/app/differentials, src/lib/differentials.ts` |
| DSM-5 Diagnosis | `src/app/dsm, src/components/dsm, src/lib/dsm.ts` |
| Specifiers (legacy redirect) | `src/app/specifiers/[[...path]]/page.tsx` |
| Specifiers | `src/app/specifiers, src/components/specifiers, src/lib/specifiers.ts` |
| Formulation | `src/app/formulation, src/components/formulation, src/lib/formulation.ts` |
| Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` |
| Documents | `src/app/documents, src/lib/document-flow-routes.ts` |
Expand Down
5 changes: 3 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
// mockup can never mask a production-journey regression (PT-05). The two
// Tag-level filters keep production and prototype journeys disjoint even when
// they share a spec file; firefox/webkit retain the legacy full testMatch.
const productionSpecPattern = /.*ui-(smoke|stress|accessibility|tools|overlap|universal-search|formulation)\.spec\.ts/;
const productionSpecPattern =
/.*ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation)\.spec\.ts/;
const mockupSpecPattern = /.*ui-(tools|tools-collapse|tools-task-directory)\.spec\.ts/;
const mockupTag = /@mockup/;

export default defineConfig({
testDir: "./tests",
testMatch:
/.*ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|formulation)\.spec\.ts/,
/.*ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation)\.spec\.ts/,
timeout: 60_000,
retries: process.env.CI ? 1 : 0,
expect: {
Expand Down
18 changes: 15 additions & 3 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { dsmDiagnoses } from "@/lib/dsm";
import { formulationMechanisms } from "@/lib/formulation";
import { formRecords } from "@/lib/forms";
import { serviceRecords } from "@/lib/services";
import { specifierRecords } from "@/lib/specifiers";

const appDir = path.join(process.cwd(), "src", "app");
const siteMapPath = path.join(process.cwd(), "docs", "site-map.md");
Expand Down Expand Up @@ -63,7 +64,11 @@ const routeDescriptions: Record<string, string> = {
"/formulation/builder": "Structured clinical formulation builder.",
"/formulation/compare": "Side-by-side mechanism comparison.",
"/formulation/map": "Formulation mechanism domain map.",
"/specifiers/[[...path]]": "Compatibility redirect to the corrected Formulation workspace.",
"/specifiers": "Psychiatric specifier home and local search surface.",
"/specifiers/[slug]": "Psychiatric specifier decision-support guide.",
"/specifiers/builder": "Structured diagnostic wording builder.",
"/specifiers/compare": "Side-by-side psychiatric specifier comparison.",
"/specifiers/map": "Psychiatric specifier family map.",
};

const apiDescriptions: Record<string, string> = {
Expand Down Expand Up @@ -104,7 +109,7 @@ const routeOwnershipRows = [
["Favourites", "src/app/favourites, src/components/clinical-dashboard/favourites-command-library-page.tsx"],
["Differentials", "src/app/differentials, src/lib/differentials.ts"],
["DSM-5 Diagnosis", "src/app/dsm, src/components/dsm, src/lib/dsm.ts"],
["Specifiers (legacy redirect)", "src/app/specifiers/[[...path]]/page.tsx"],
["Specifiers", "src/app/specifiers, src/components/specifiers, src/lib/specifiers.ts"],
["Formulation", "src/app/formulation, src/components/formulation, src/lib/formulation.ts"],
["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"],
["Documents", "src/app/documents, src/lib/document-flow-routes.ts"],
Expand Down Expand Up @@ -293,7 +298,7 @@ function renderModePageIndex() {
mode: "Specifiers",
home: appModeHomeHref("specifiers"),
search: appModeHomeHref("specifiers", { query: "depressed but racing thoughts", focus: true, run: true }),
detail: "Compatibility entry that redirects through `/specifiers` into the Formulation workspace.",
detail: "`/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, and `/specifiers/map`.",
},
{
mode: "Formulation",
Expand Down Expand Up @@ -349,6 +354,7 @@ function renderSiteMapRaw(data = collectSiteMapData()) {
"/services/[slug]",
"/forms/[slug]",
"/differentials/diagnoses/[slug]",
"/specifiers/[slug]",
"/dsm/diagnoses/[slug]",
"/dsm/diagnoses/[slug]/differentials",
"/formulation/[slug]",
Expand Down Expand Up @@ -389,6 +395,12 @@ function renderSiteMapRaw(data = collectSiteMapData()) {
serviceRecords.map((record) => record.slug),
),
"",
...renderSlugInventory(
"Psychiatric specifier slugs",
"/specifiers/[slug]",
specifierRecords.map((record) => record.slug),
),
"",
...renderSlugInventory(
"Seeded form slugs",
"/forms/[slug]",
Expand Down
68 changes: 0 additions & 68 deletions src/app/specifiers/[[...path]]/page.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions src/app/specifiers/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { notFound } from "next/navigation";
import type { Metadata } from "next";

import { SpecifierRecordPage } from "@/components/specifiers/specifier-record-page";
import { findSpecifier, specifierRecords } from "@/lib/specifiers";

export function generateStaticParams() {
return specifierRecords.map((record) => ({ slug: record.slug }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const record = findSpecifier(slug);
if (!record) return {};

return {
title: record.name,
description: record.summary,
};
}

export default async function SpecifierDetailRoute({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const record = findSpecifier(slug);
if (!record) notFound();

return <SpecifierRecordPage record={record} />;
}
16 changes: 16 additions & 0 deletions src/app/specifiers/builder/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SpecifierBuilderPage } from "@/components/specifiers/specifier-builder-page";

type BuilderRouteProps = {
searchParams?: Promise<{ specifier?: string | string[] }>;
};

export default async function SpecifierBuilderRoute({ searchParams }: BuilderRouteProps) {
const params = searchParams ? await searchParams : {};
const initialSpecifiers = Array.isArray(params.specifier)
? Array.from(new Set(params.specifier))
: params.specifier
? [params.specifier]
: [];

return <SpecifierBuilderPage initialSpecifiers={initialSpecifiers} />;
}
35 changes: 35 additions & 0 deletions src/app/specifiers/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";

import { SpecifierComparePage } from "@/components/specifiers/specifier-compare-page";
import { findSpecifier } from "@/lib/specifiers";

type CompareRouteProps = {
searchParams?: Promise<{ a?: string | string[]; b?: string | string[] }>;
};

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

export async function generateMetadata({ searchParams }: CompareRouteProps): Promise<Metadata> {
const params = searchParams ? await searchParams : {};
const left = findSpecifier(first(params.a) ?? "");
const right = findSpecifier(first(params.b) ?? "");

if (left && right) {
return {
title: `Compare ${left.shortName} and ${right.shortName}`,
description: `Side-by-side comparison of ${left.name} and ${right.name} specifiers.`,
};
}

return {
title: "Compare specifiers",
description: "Compare psychiatric specifiers side by side to understand the deciding features.",
};
}

export default async function SpecifierCompareRoute({ searchParams }: CompareRouteProps) {
const params = searchParams ? await searchParams : {};
return <SpecifierComparePage initialLeft={first(params.a)} initialRight={first(params.b)} />;
}
11 changes: 11 additions & 0 deletions src/app/specifiers/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { ReactNode } from "react";

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

export default function SpecifiersLayout({ children }: { children: ReactNode }) {
return (
<GlobalSearchShell initialMode="specifiers" desktopSearchPlacement="hero">
{children}
</GlobalSearchShell>
);
}
19 changes: 19 additions & 0 deletions src/app/specifiers/map/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";

import { SpecifierMapPage } from "@/components/specifiers/specifier-map-page";

type MapRouteProps = {
searchParams?: Promise<{ selected?: string | string[] }>;
};

export const metadata: Metadata = {
title: "Specifier map",
description:
"Browse psychiatric specifiers by diagnostic architecture: episode features, course and onset, severity and remission.",
};

export default async function SpecifierMapRoute({ searchParams }: MapRouteProps) {
const params = searchParams ? await searchParams : {};
const selected = Array.isArray(params.selected) ? params.selected[0] : params.selected;
return <SpecifierMapPage initialSlug={selected} />;
}
17 changes: 17 additions & 0 deletions src/app/specifiers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SpecifiersHomePage } from "@/components/specifiers/specifiers-home-page";

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

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

export default async function SpecifiersRoute({ searchParams }: SpecifiersRouteProps) {
const params = searchParams ? await searchParams : {};
const query = (firstSearchParam(params.q) ?? firstSearchParam(params.query) ?? "").trim();
const hasSubmittedSearch = firstSearchParam(params.run) === "1" && query.length > 0;

return <SpecifiersHomePage query={query} autoRunSearch={hasSubmittedSearch} />;
}
Loading
Loading