Skip to content
Closed
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
28 changes: 9 additions & 19 deletions src/components/clinical-dashboard/differentials-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useDifferentialSearch } from "@/components/clinical-dashboard/use-diffe
import { useResultSort } from "@/components/use-result-sort";
import { cn } from "@/components/ui-primitives";
import { appModeHomeHref } from "@/lib/app-modes";
import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation";
import { differentialsMobileCompareAddonSlotId } from "@/lib/mode-home-composer";
import {
composeDifferentialSearchResults,
Expand Down Expand Up @@ -111,17 +112,6 @@ const candidateIconBySlug: Array<[string, LucideIcon]> = [
["delirium", BrainCircuit],
];

function routeWithQuery(path: string, query: string, selectedIds?: Set<string>) {
const params = new URLSearchParams();
const trimmedQuery = query.trim();
if (trimmedQuery) params.set("q", trimmedQuery);
if (selectedIds && selectedIds.size > 0) {
params.set("ids", Array.from(selectedIds).join(","));
}
const suffix = params.toString();
return suffix ? `${path}?${suffix}` : path;
}

/**
* Mobile/tablet floating compare action. Portals into the search composer's
* addon slot so it stays anchored beside the active result controls, but
Expand Down Expand Up @@ -162,7 +152,7 @@ function DifferentialsMobileCompareBar({
<div aria-live="polite" className="flex w-full justify-center">
{hasSelection ? (
<Link
href={routeWithQuery("/differentials/presentations", query, selectedIds)}
href={differentialSelectedCompareHref(query, selectedIds)}
data-testid="differentials-compare-selected-mobile"
className="inline-flex min-h-12 max-w-full items-center gap-2.5 rounded-full border border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] py-1 pl-4 pr-2.5 text-sm font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-elevated)] transition active:bg-[color:var(--clinical-accent-hover)]"
>
Expand Down Expand Up @@ -599,7 +589,7 @@ function SafetyCard({ safety, query }: { safety: string; query: string }) {
</h2>
<p className="mt-2 text-sm font-semibold leading-6 text-[color:var(--text-heading)]">{safety}</p>
<Link
href={routeWithQuery("/differentials/presentations", query)}
href={differentialRouteWithQuery("/differentials/presentations", query)}
className="mt-2 inline-flex min-h-tap items-center gap-1.5 text-sm font-bold text-[color:var(--clinical-accent)]"
>
View presentation guide
Expand Down Expand Up @@ -929,14 +919,14 @@ function SearchResultsView({
</p>
<div className="flex flex-wrap gap-2">
<Link
href={routeWithQuery("/differentials/presentations", query)}
href={differentialRouteWithQuery("/differentials/presentations", query)}
className="inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-sm font-extrabold text-[color:var(--clinical-accent)]"
>
Browse presentations
<ChevronRight className="h-4 w-4" aria-hidden />
</Link>
<Link
href={routeWithQuery("/differentials/diagnoses", query)}
href={differentialRouteWithQuery("/differentials/diagnoses", query)}
className="inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-sm font-extrabold text-[color:var(--clinical-accent)]"
>
Browse diagnoses
Expand Down Expand Up @@ -1097,7 +1087,7 @@ function SearchResultsView({
</div>

<Link
href={routeWithQuery("/differentials/diagnoses", query)}
href={differentialRouteWithQuery("/differentials/diagnoses", query)}
className="hidden min-h-11 w-full items-center justify-center gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm font-extrabold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] lg:inline-flex"
>
View all catalogue matches ({results.length})
Expand All @@ -1106,7 +1096,7 @@ function SearchResultsView({

{selectedCount > 0 ? (
<Link
href={routeWithQuery("/differentials/presentations", query, comparisonIds)}
href={differentialSelectedCompareHref(query, comparisonIds)}
className="hidden min-h-14 w-full items-center justify-center gap-3 rounded-lg bg-[color:var(--clinical-accent)] px-4 text-base font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-elevated)] transition hover:bg-[color:var(--clinical-accent-hover)] lg:inline-flex"
>
<GitCompareArrows className="h-5 w-5" aria-hidden />
Expand Down Expand Up @@ -1205,12 +1195,12 @@ export function DifferentialsHome({
function handleAction(action: DifferentialAction) {
if (action.target === "presentations") {
if (onOpenPresentations) onOpenPresentations(action.query);
else router.push(routeWithQuery("/differentials/presentations", action.query));
else router.push(differentialRouteWithQuery("/differentials/presentations", action.query));
return;
}
if (action.target === "diagnoses") {
if (onOpenDiagnoses) onOpenDiagnoses(action.query);
else router.push(routeWithQuery("/differentials/diagnoses", action.query));
else router.push(differentialRouteWithQuery("/differentials/diagnoses", action.query));
return;
}
runSearch(action.query);
Expand Down
20 changes: 20 additions & 0 deletions src/lib/differentials-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid importing the full snapshot in client href helper

When DifferentialsHome value-imports this helper from the client component, this import follows @/lib/differentialsdifferential-fixturesdata/differentials-snapshot.json, so simply opening the differentials dashboard can pull the full ~1.2 MB generated catalogue into the browser bundle. That regresses the repo's fixture-free client boundary for the dashboard; the smallest proof is a production build plus npm run check:bundle-budget, or extending tests/client-performance-boundaries.test.ts to catch this transitive import. Keep this lookup server-side or move only the small diagnosis-to-workflow mapping needed for hrefs into a client-safe module.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Replaced the compare href helper’s transitive @/lib/differentials import with a client-safe workflow lookup module, so dashboard navigation no longer pulls the generated differential catalogue into the client graph. src/lib/differentials-navigation.tsL1-L4
  • Added a compact diagnosis-to-presentation workflow mapping plus selection helper for client-side compare URL construction, keeping it independent of differential-fixtures and the snapshot JSON. src/lib/differentials-navigation-workflows.tsL1-L60
  • Extended the fixture-free client boundary test to assert that differential navigation does not import @/lib/differentials, differential-fixtures, differentials-snapshot, or loadDifferentialSnapshot. tests/client-performance-boundaries.test.tsL18-L25
  • Committed the scoped fix on the current branch: dd4c1f6 fix(differentials): keep compare navigation client-safe.
  • Created PR metadata with title: Fix(differentials): keep compare navigation client-safe.

Testing

  • ⚠️ npm run test -- tests/differentials-navigation.test.ts tests/client-performance-boundaries.test.ts — initially could not run because dependencies were not installed and vitest was missing from node_modules.
  • ⚠️ npm ci — initially failed under Node v20.20.2 because the repo requires Node 24.x.
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm ci
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run test -- tests/differentials-navigation.test.ts tests/client-performance-boundaries.test.ts
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run typecheck
  • export PATH="$HOME/.nvm/versions/node/v24.15.0/bin:$PATH"; npm run lint
  • git diff --check

Worktree

  • Current branch: work.
  • Worktree is clean after commit.

View task →

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep fixture catalog out of client navigation

When the /differentials client component imports differentials-navigation, this value import pulls in @/lib/differentials, which imports differential-fixtures and evaluates differentialRecords = catalog().diagnoses; that loads data/differentials-snapshot.json into the client graph (~1.2 MB raw) just to build a compare href. This bypasses the existing fixture-free client boundary because the dashboard no longer imports @/lib/differentials directly but still reaches it through this helper, so the differentials home page pays a large bundle/parse cost for every user. Split out a tiny client-safe diagnosis→workflow map or keep workflow resolution on the server/redirect path instead of importing the full catalog here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Replaced the client navigation helper’s transitive @/lib/differentials value import with a client-safe static workflow candidate map, preserving direct presentation href resolution without pulling the generated catalogue into the client graph. src/lib/differentials-navigation.tsL1-L45
  • Added src/lib/differential-presentation-workflow-map.ts as the small browser-safe diagnosis-to-presentation index used by compare navigation. src/lib/differential-presentation-workflow-map.tsL1-L10
  • Extended the fixture-free client performance boundary test to assert both the navigation helper and workflow map avoid @/lib/differentials, fixtures, and snapshot imports. tests/client-performance-boundaries.test.tsL18-L32
  • Kept the focused navigation test proving selected compare hrefs still route directly to presentation pages and preserve query/IDs without 0.0.0.0. tests/differentials-navigation.test.tsL12-L21
  • Committed the fix on the current branch: 500028a Fix client-safe differentials navigation.
  • Created PR metadata with make_pr titled: Fix client-safe differentials compare navigation.

Testing

  • npm run test -- tests/differentials-navigation.test.ts tests/client-performance-boundaries.test.ts — failed before dependencies were installed: missing node_modules/vitest/vitest.mjs.
  • npm ci — failed under Node v20.20.2 because this repo requires Node 24.x / npm 11.x.
  • source ~/.nvm/nvm.sh && nvm use 24.15.0 >/dev/null && npm ci
  • source ~/.nvm/nvm.sh && nvm use 24.15.0 >/dev/null && npm run test -- tests/differentials-navigation.test.ts tests/client-performance-boundaries.test.ts && npm run typecheck

Status

  • Current branch: work.
  • Worktree is clean after commit.

View task →


export function differentialRouteWithQuery(path: string, query: string, selectedIds?: Iterable<string>) {
const params = new URLSearchParams();
const trimmedQuery = query.trim();
if (trimmedQuery) params.set("q", trimmedQuery);
const ids = selectedIds ? Array.from(selectedIds, (id) => id.trim()).filter(Boolean) : [];
if (ids.length > 0) params.set("ids", ids.join(","));
const suffix = params.toString();
return suffix ? `${path}?${suffix}` : path;
}

export function differentialSelectedCompareHref(query: string, selectedIds: Iterable<string>) {
const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds);
return differentialRouteWithQuery(
`/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`,
query,
selection?.diagnosisIds,
Comment on lines +13 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve selected IDs on the fallback route.

If workflow resolution returns undefined, this code routes to acute-confusion-encephalopathy but passes undefined for selectedIds, so the generated URL omits the user’s selected diagnoses. Normalize the iterable once before resolution and fall back to those IDs when no workflow selection exists. Add a regression test for an unmapped or whitespace-padded ID.

Proposed fix
 export function differentialSelectedCompareHref(query: string, selectedIds: Iterable<string>) {
-  const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds);
+  const normalizedIds = Array.from(selectedIds, (id) => id.trim()).filter(Boolean);
+  const selection = getPresentationWorkflowSelectionForDiagnosisIds(normalizedIds);
   return differentialRouteWithQuery(
     `/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`,
     query,
-    selection?.diagnosisIds,
+    selection?.diagnosisIds ?? normalizedIds,
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function differentialSelectedCompareHref(query: string, selectedIds: Iterable<string>) {
const selection = getPresentationWorkflowSelectionForDiagnosisIds(selectedIds);
return differentialRouteWithQuery(
`/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`,
query,
selection?.diagnosisIds,
export function differentialSelectedCompareHref(query: string, selectedIds: Iterable<string>) {
const normalizedIds = Array.from(selectedIds, (id) => id.trim()).filter(Boolean);
const selection = getPresentationWorkflowSelectionForDiagnosisIds(normalizedIds);
return differentialRouteWithQuery(
`/differentials/presentations/${selection?.workflow.id ?? "acute-confusion-encephalopathy"}`,
query,
selection?.diagnosisIds ?? normalizedIds,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/differentials-navigation.ts` around lines 13 - 18, Update
differentialSelectedCompareHref to normalize selectedIds once before calling
getPresentationWorkflowSelectionForDiagnosisIds, then pass the normalized IDs to
differentialRouteWithQuery when workflow resolution is undefined while
preserving selection?.diagnosisIds for resolved workflows. Add a regression test
covering an unmapped or whitespace-padded ID and verifying the fallback URL
retains the selected diagnosis IDs.

);
}
20 changes: 20 additions & 0 deletions tests/differentials-navigation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";

import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation";

describe("differentials navigation", () => {
it("builds same-origin relative query routes", () => {
expect(differentialRouteWithQuery("/differentials/diagnoses", " acute confusion ")).toBe(
"/differentials/diagnoses?q=acute+confusion",
);
});

it("wires selected comparison directly to a presentation page instead of the redirect endpoint", () => {
const href = differentialSelectedCompareHref("Pain", new Set(["anorexia-nervosa", "bulimia-nervosa-binge-purge-pattern"]));

expect(href).toMatch(/^\/differentials\/presentations\/[^?]+\?/);
expect(href).toContain("q=Pain");
expect(href).toContain("ids=anorexia-nervosa%2Cbulimia-nervosa-binge-purge-pattern");
expect(href).not.toContain("0.0.0.0");
});
});
Loading