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
63 changes: 33 additions & 30 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/codebase-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ One shared composer (`master-search-header.tsx`) serves every mode. Placement:
- **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow.
- **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up.
- **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` → `ServicesNavigatorPage`, `/forms` → `FormsSearchResultsPage`, `/differentials` → `DifferentialsHome` results view, `/formulation` → local mechanism results, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` — intentional, since they need retrieval/answer state. Bare `/?mode=<id>` always renders the shared home with that mode preselected; only a submitted deep link (`q` plus `run=1`) resolves to the mode's own search surface (proxy early-redirect still covers favourites/differentials/specifiers for those submitted aliases).
- **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits.
- **Intentionally composer-free routes**: `/differentials/presentations/*` and `/differentials/compare` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits.
- **Shared secondary navigation**: `src/components/secondary-navigation.tsx` (`SecondaryNavigation`, route/section/action items, roving tablist, fragment section tracking) and `src/components/page-secondary-navigation.tsx` (`PageSecondaryNavigation`, per-route selection of mode destinations vs. "On this page" section anchors). Mode destinations come from `src/lib/mode-secondary-navigation.ts` (`modeSecondaryNavigationRegistry`, no "Home" item). `GlobalSearchShell` renders it in normal flow at the top of `#main-content` for its owned namespaced modes; it self-suppresses on clean mode homes, locally-owned detail routes (medications, factsheets, differentials diagnoses) and Therapy Compass, and Specifiers/Formulation keep their existing local `Subnav` (so the shared mode bar is skipped for those two modes to avoid a duplicate row).
- **Local filter fields** (sidebar "Search chats", document drawer "Find a document"/"Find a source PDF") are scoped filters, not global search; they share the `fieldControlWithIcon`/`fieldIcon` primitives.
- **Wiring conventions** for buttons and route navigation (and the gates that enforce them — the dead-button ESLint rule and the orphan-route reachability test) live in `docs/wiring-conventions.md`.
Expand Down
8 changes: 4 additions & 4 deletions docs/design-system/ADOPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,12 @@ product exclusions; the only route-only disposition is the documented legacy doc
redirect. Shared shell/component roots carry their own explicit `shared-shell` disposition.

Registered public components: 53
Declared product roots: 58
Declared product roots: 59
Roots with a literal `.ckb-v2` opt-in: 1
Roots inheriting `.ckb-v2` from the global `<html>`: 57
Roots inheriting `.ckb-v2` from the global `<html>`: 58
Production surfaces observed under v2: 14/14
Dynamic `ckb-v2` constructions: 0
Declared production page routes: 50/50
Declared production page routes: 51/51

Source observation and contract declaration are independent. A literal `ckb-v2` on the global `<html>` makes every production surface inherit v2, but it does not approve that adoption.
The Proof column summarizes each surface's dark, forced-colours, 320px, print and browser declarations; exact statuses and evidence paths live in the manifest.
Expand All @@ -342,7 +342,7 @@ Observed v2 under a compatibility declaration fails closed. A declared v2 shell
| ---------------------------------- | --------------- | -----: | ----: | -------------- | -------------------------- | -------------- | -------------- |
| `root-shell-and-settings` | shared-shell | 3 | 6 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `catalogues-forms-and-info` | owned | 13 | 13 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `differentials` | owned | 5 | 5 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `differentials` | owned | 6 | 6 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `formulation` | owned | 5 | 5 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `specifiers` | owned | 5 | 5 | v2 | v2 (inherited-global-root) | passed | not-committed |
| `therapy-compass` | owned | 9 | 10 | v2 | v2 (inherited-global-root) | passed | not-committed |
Expand Down
1 change: 1 addition & 0 deletions docs/design-system/adoption-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
"id": "differentials",
"disposition": "owned",
"routes": [
"src/app/(search-app)/differentials/compare/page.tsx",
"src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx",
"src/app/(search-app)/differentials/diagnoses/page.tsx",
"src/app/(search-app)/differentials/page.tsx",
Expand Down
21 changes: 17 additions & 4 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,7 @@
"disposition": "owned",
"documentedDisposition": null,
"routes": [
"src/app/(search-app)/differentials/compare/page.tsx",
"src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx",
"src/app/(search-app)/differentials/diagnoses/page.tsx",
"src/app/(search-app)/differentials/page.tsx",
Expand Down Expand Up @@ -2249,6 +2250,16 @@
"permittedComponentFamilies": ["controls", "feedback", "layout", "source"],
"sanctionedSpecialPatterns": ["Differential"],
"roots": [
{
"file": "src/app/(search-app)/differentials/compare/page.tsx",
"exists": true,
"imports": [],
"importedFamilies": [],
"literalCkbV2": false,
"dynamicCkbV2": false,
"v2MountMode": "inherited-global-root",
"sanctionedPatternsPresent": ["Differential"]
},
{
"file": "src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx",
"exists": true,
Expand Down Expand Up @@ -3182,6 +3193,7 @@
"routeCoverage": {
"discovered": [
"src/app/(search-app)/calculators/page.tsx",
"src/app/(search-app)/differentials/compare/page.tsx",
"src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx",
"src/app/(search-app)/differentials/diagnoses/page.tsx",
"src/app/(search-app)/differentials/page.tsx",
Expand Down Expand Up @@ -3234,6 +3246,7 @@
],
"declared": [
"src/app/(search-app)/calculators/page.tsx",
"src/app/(search-app)/differentials/compare/page.tsx",
"src/app/(search-app)/differentials/diagnoses/[slug]/page.tsx",
"src/app/(search-app)/differentials/diagnoses/page.tsx",
"src/app/(search-app)/differentials/page.tsx",
Expand Down Expand Up @@ -3290,7 +3303,7 @@
},
"adoption": {
"literalCkbV2RootCount": 1,
"inheritedCkbV2RootCount": 57,
"inheritedCkbV2RootCount": 58,
"dynamicCkbV2RootCount": 0,
"v2MountedSurfaceCount": 14,
"declaredV2SurfaceCount": 14
Expand All @@ -3299,8 +3312,8 @@
"registeredComponentCount": 53,
"previewCount": 53,
"productImportedComponentCount": 31,
"rootCount": 58,
"productionRouteCount": 50,
"nextUiEntryCount": 102
"rootCount": 59,
"productionRouteCount": 51,
"nextUiEntryCount": 103
}
}
2 changes: 1 addition & 1 deletion docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/` - Main Clinical KB shell. Source: `src/app/(search-app)/page.tsx`.
- `/calculators` - Route discovered from app directory Source: `src/app/(search-app)/calculators/page.tsx`.
- `/differentials` - Differentials home and search surface. Source: `src/app/(search-app)/differentials/page.tsx`.
- `/differentials/compare` - Compare entry: same-presentation selections redirect into a catalogue workflow; cross-presentation selections render an ad-hoc comparison. Source: `src/app/(search-app)/differentials/compare/page.tsx`.
- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/(search-app)/differentials/diagnoses/page.tsx`.
- `/differentials/presentations` - Presentation catalogue stream. Source: `src/app/(search-app)/differentials/presentations/page.tsx`.
- `/differentials/presentations/[slug]` - Presentation comparison workflow. Source: `src/app/(search-app)/differentials/presentations/[slug]/page.tsx`.
Expand Down Expand Up @@ -1101,7 +1102,6 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
## Redirects

- `/applications` - Redirects to `/tools`. Source: `src/app/applications/route.ts`.
- `/differentials/compare` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/(search-app)/differentials/compare/route.ts`.
- `/documents/source` - Redirects to `/documents/[id]`. Source: `src/app/(search-app)/documents/source/page.tsx`.
- `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`.
- `/mockups/medication-prescribing` - Redirects to `/medications/acamprosate`. Source: `src/app/mockups/medication-prescribing/page.tsx`.
Expand Down
6 changes: 3 additions & 3 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ type SiteMapData = {
nonRoutedMockupArtifacts: string[];
};

const productRouteHandlerPaths = new Set(["/applications", "/differentials/compare"]);
const productRouteHandlerPaths = new Set(["/applications"]);

const documentedRedirectTargets: Record<string, string> = {
"/applications": "/tools",
"/differentials/compare": "/differentials/presentations/[workflow-slug]",
// The source page redirects a valid id to the canonical `/documents/[id]` viewer
// (page.tsx line 20) and only falls back to `/documents/search` for an invalid id
// (line 14). Pin the canonical target here so the generated map does not report the
Expand All @@ -54,11 +53,12 @@ const routeDescriptions: Record<string, string> = {
"/": "Main Clinical KB shell.",
"/applications": "Legacy application launcher redirect to Tools.",
"/differentials": "Differentials home and search surface.",
"/differentials/compare":
"Compare entry: same-presentation selections redirect into a catalogue workflow; cross-presentation selections render an ad-hoc comparison.",
"/differentials/diagnoses": "Diagnosis stream.",
"/differentials/diagnoses/[slug]": "Differential diagnosis detail.",
"/differentials/presentations": "Presentation catalogue stream.",
"/differentials/presentations/[slug]": "Presentation comparison workflow.",
"/differentials/compare": "Compare entry redirect into a presentation workflow.",
"/dsm": "DSM-5 Diagnosis home.",
"/dsm/search": "DSM diagnosis search and catalogue browser.",
"/dsm/compare": "DSM diagnosis comparison.",
Expand Down
49 changes: 49 additions & 0 deletions src/app/(search-app)/differentials/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";

import { DifferentialPresentationWorkflowPage } from "@/components/differentials/differential-presentation-workflow-page";
import { resolveDifferentialCompareHandoff } from "@/lib/differentials";

export const metadata: Metadata = {
title: "Compare differentials | Clinical KB",
description: "Compare selected differential diagnoses side by side with safety and bedside context.",
};

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

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

/**
* Compare entry page.
*
* Same-presentation selections (and bare/unknown ids) redirect into a catalogue
* presentation workflow. Cross-presentation selections render an ad-hoc compare
* view here so every valid id is preserved. A competing `route.ts` at this path
* is invalid in the App Router — handoff lives in the page instead.
*/
export default async function DifferentialCompareRoute({ searchParams }: DifferentialCompareRouteProps) {
const resolvedSearchParams = searchParams ? await searchParams : {};
const query = firstSearchParam(resolvedSearchParams.query ?? resolvedSearchParams.q)?.trim() ?? "";
const selectedIds = (firstSearchParam(resolvedSearchParams.ids) ?? "")
.split(",")
.map((value) => value.trim())
.filter(Boolean);

const handoff = resolveDifferentialCompareHandoff(selectedIds, query);
if (handoff.kind === "presentation") {
redirect(handoff.href);
}

return (
<DifferentialPresentationWorkflowPage
query={query}
presentationSlug={handoff.selection.workflow.id}
selectedIds={handoff.selection.diagnosisIds}
workflow={handoff.selection.workflow}
/>
);
}
31 changes: 0 additions & 31 deletions src/app/(search-app)/differentials/compare/route.ts

This file was deleted.

41 changes: 32 additions & 9 deletions src/components/clinical-dashboard/differentials-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import { useResultSort } from "@/components/use-result-sort";
import { Chip as DesignChip } from "@/components/ui/chip";
import { cn } from "@/components/ui-primitives";
import { appModeHomeHref } from "@/lib/app-modes";
import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation";
import {
differentialIdsFromSearchParams,
differentialRouteWithQuery,
differentialSelectedCompareHref,
syncDifferentialSelectionIdsToUrl,
} from "@/lib/differentials-navigation";
import { differentialsMobileCompareAddonSlotId } from "@/lib/mode-home-composer";
import {
composeDifferentialSearchResults,
Expand Down Expand Up @@ -819,6 +824,12 @@ function SearchResultsView({
const filterPanelId = useId();
const [filterOpen, setFilterOpen] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(() => new Set());
// Capture cold-load URL ids in state so a loading catalogue cannot wipe `ids`
// via replaceState before the first result set hydrates selection.
const [initialUrlIds] = useState(() =>
typeof window === "undefined" ? [] : differentialIdsFromSearchParams(window.location.search),
);
const [urlHydrationPending, setUrlHydrationPending] = useState(() => initialUrlIds.length > 0);
// Selection, filter, and sort follow the ranked result set: seed the top two
// for comparison and drop stale ids whenever a new query changes the results
// (render-time sync, matching the repo's set-state-in-render pattern).
Expand All @@ -827,14 +838,16 @@ function SearchResultsView({
if (lastResultSignature !== resultSignature) {
setLastResultSignature(resultSignature);
setKindFilter("all");
setSelectedIds(
new Set(
results
.filter((result) => result.kind === "diagnosis")
.slice(0, 2)
.map((result) => result.id),
),
);
const diagnosisIds = results.filter((result) => result.kind === "diagnosis").map((result) => result.id);
const diagnosisIdSet = new Set(diagnosisIds);
// First result set may hydrate shareable URL ids; later query changes always
// re-seed so a new scope never silently inherits the previous ticks.
const urlIds = urlHydrationPending ? initialUrlIds.filter((id) => diagnosisIdSet.has(id)) : [];
const nextIds = lastResultSignature === "" && urlIds.length > 0 ? urlIds : diagnosisIds.slice(0, 2);
if (urlHydrationPending && lastResultSignature === "" && resultSignature !== "") {
setUrlHydrationPending(false);
}
setSelectedIds(new Set(nextIds));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const presentationCount = results.filter((result) => result.kind === "presentation").length;
Expand All @@ -861,6 +874,16 @@ function SearchResultsView({
[results, selectedIds],
);
const selectedCount = comparisonIds.size;
const comparisonIdsKey = Array.from(comparisonIds).join(",");

// Publish selection into the URL so ModeNav Compare can forward the same ids.
// Defer while the catalogue is still loading so a cold submitted-search load
// does not delete bookmarked `ids` before matches arrive for hydration.
useEffect(() => {
if (typeof window === "undefined") return;
if (catalog.status === "loading") return;
syncDifferentialSelectionIdsToUrl(comparisonIdsKey ? comparisonIdsKey.split(",") : []);
Comment thread
cursor[bot] marked this conversation as resolved.
}, [catalog.status, comparisonIdsKey]);
// Catalogue results follow composer edits live, but document evidence only
// updates on an executed source search — treat evidence fetched for a
// different query as pending so the two panels never claim to be in sync.
Expand Down
Loading
Loading