Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
648abfa
docs: align readiness runtime version
BigSimmo Jul 10, 2026
4249b3c
feat(differentials): add detail-page helpers, server context, shared …
BigSimmo Jul 10, 2026
fb2aceb
feat(differentials): interactive diagnosis detail page
BigSimmo Jul 10, 2026
50e60de
test(differentials): e2e coverage for the diagnosis detail page; skip…
BigSimmo Jul 10, 2026
65ea4dc
fix(differentials): clean Watch-for tag display; harden detail e2e fi…
BigSimmo Jul 10, 2026
73a6838
Merge remote-tracking branch 'origin/main' into claude/differentials-…
BigSimmo Jul 11, 2026
3674f9f
fix(scripts): port the reserved-dev-port skip to the relocated local-…
BigSimmo Jul 11, 2026
7c94fae
Merge remote-tracking branch 'origin/main' into claude/differentials-…
BigSimmo Jul 11, 2026
947e836
fix(lib): fail the clipboard fallback when execCommand is unavailable
BigSimmo Jul 11, 2026
53faf48
Merge remote-tracking branch 'origin/main' into claude/differentials-…
BigSimmo Jul 11, 2026
36cca1b
fix(differentials): ship detail context with the API record; expose s…
BigSimmo Jul 11, 2026
40342da
fix: derive differential detail from owner catalog
BigSimmo Jul 11, 2026
a96821b
fix: link only routable differential records
BigSimmo Jul 11, 2026
aa794ba
test: stabilize flaky UI assertions
Copilot Jul 11, 2026
31398bd
test: fix flaky ci ui assertions
Copilot Jul 11, 2026
061c888
Merge remote-tracking branch 'origin/main' into codex/pr-483-fixes
BigSimmo Jul 11, 2026
0c32a24
Merge remote-tracking branch 'origin/main' into codex/pr-483-fixes
BigSimmo Jul 11, 2026
0569b63
test: assert current differential response
BigSimmo Jul 11, 2026
190afff
fix: address current differential review findings
BigSimmo Jul 11, 2026
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 @@ -40,3 +40,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-11 | PR #487 / claude/answer-page-design-polish-ffd5a6 | b2c772606126f8323424bc9c0b636bac77c08789 | open-PR review, unresolved comments, and CI | Two findings fixed: expanded weak/unsupported prior answers retain an explicit source-review warning, and cross-mode search actions no longer log an incorrect detail-open telemetry event. Added a persisted prior-turn browser assertion. No additional high-confidence defect was found in the changed scope. | Focused answer-render and cross-mode Vitest (20/20); TypeScript; focused Prettier; `git diff --check`. Browser assertion delegated to hosted CI because Turbopack rejects the isolated worktree's external node_modules junction. |
| 2026-07-11 | PR #469 / claude/response-formatting-cleanup-b57a9c | f1864308e0b287bb83b2a13daca4c3aa2ab95a3e | open-PR review, unresolved comments, and CI | P2 fixed: the OCR bullet sanitizer now preserves line-start O blood values when followed by blood or red-cell noun tails while still stripping non-blood bullets such as `o Negative screen`. No additional high-confidence defect was found in the nine-file diff. | Focused sanitizer/extractive Vitest (75/75); TypeScript; focused Prettier; `git diff --check`. Production-readiness script ran fail-closed with provider variables cleared and reported only expected missing provider configuration. |
| 2026-07-11 | PR #466 / claude/search-timeout-failure-s6aiuj | 54d52292eeb9e1c7856b3dad89d1b72e0d49fd53 | open-PR review, unresolved comments, and CI | P2 fixed: SSE progress/token/error emission now tolerates a client cancellation racing an enqueue, so the catch path cannot throw while reporting the original stream error. No additional high-confidence defect was found in the six-file diff. | Focused SSE and search utility Vitest (13/13); TypeScript; focused Prettier. Hosted advisory browser failure was shared stale assertion drift and is rerun after this push. |
| 2026-07-11 | PR #483 / claude/differentials-page-review-a3daaf | 36cca1bf7c13718dcc60a61b75272c7c4fa5cd44 | open-PR review, unresolved comments, and CI | P2 fixed: authenticated diagnosis detail responses now derive related links, overlap links, and comparison presentation from the owner's current diagnosis and presentation rows rather than the bundled snapshot. Added an owner-only catalog regression test. No additional high-confidence defect was found in the changed scope. | Focused differentials route/catalog Vitest (26/26); TypeScript; focused Prettier; `git diff --check`. Production readiness ran fail-closed with provider variables cleared and reported only expected missing provider configuration. |
3 changes: 2 additions & 1 deletion scripts/dev-free-port.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { appName, stableProjectPort } from "../src/lib/local-server-utils.mjs";
import { appName, isReservedDevPort, stableProjectPort } from "../src/lib/local-server-utils.mjs";

if (Number(process.versions.node.split(".")[0]) !== 24) {
console.error(`Clinical KB local server requires Node 24.x. Current runtime: ${process.versions.node}.`);
Expand Down Expand Up @@ -104,6 +104,7 @@ async function canListen(port) {

async function findFreePort(preferredPort) {
for (let port = preferredPort; port <= maxPort; port += 1) {
if (isReservedDevPort(port)) continue;
Comment thread
BigSimmo marked this conversation as resolved.
if (await canListen(port)) return port;
}
throw new Error(`No free development port found from ${preferredPort} to ${maxPort}.`);
Expand Down
9 changes: 8 additions & 1 deletion scripts/ensure-local-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import http from "node:http";
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { appName, localProjectId, projectPortEnd, stableProjectPort } from "../src/lib/local-server-utils.mjs";
import {
appName,
isReservedDevPort,
localProjectId,
projectPortEnd,
stableProjectPort,
} from "../src/lib/local-server-utils.mjs";

if (Number(process.versions.node.split(".")[0]) !== 24) {
console.error(`Clinical KB local server requires Node 24.x. Current runtime: ${process.versions.node}.`);
Expand Down Expand Up @@ -147,6 +153,7 @@ async function findExistingProjectServer(startPort) {

async function findStartPort(startPort) {
for (let port = startPort; port <= maxPort; port += 1) {
if (isReservedDevPort(port)) continue;
if (await isThisProject(port, 1)) return { port, alreadyRunning: true };
if (!(await isPortBusy(port))) return { port, alreadyRunning: false };
}
Expand Down
9 changes: 8 additions & 1 deletion scripts/run-playwright.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import http from "node:http";
import net from "node:net";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { appName, localProjectId, projectPortEnd, stableProjectPort } from "../src/lib/local-server-utils.mjs";
import {
appName,
isReservedDevPort,
localProjectId,
projectPortEnd,
stableProjectPort,
} from "../src/lib/local-server-utils.mjs";

if (Number(process.versions.node.split(".")[0]) !== 24) {
console.error(`Clinical KB Playwright checks require Node 24.x. Current runtime: ${process.versions.node}.`);
Expand Down Expand Up @@ -64,6 +70,7 @@ async function canListen(port) {

async function findFreePort(startPort) {
for (let port = startPort; port <= projectPortEnd; port += 1) {
if (isReservedDevPort(port)) continue;
if (await canListen(port)) return port;
}
throw new Error(`No free Playwright server port found from ${startPort} to ${projectPortEnd}.`);
Expand Down
27 changes: 25 additions & 2 deletions src/app/api/differentials/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
type DifferentialRecordRow,
} from "@/lib/differential-records";
import { ensureDifferentialsSeeded, loadDifferentialSnapshot } from "@/lib/differential-seed";
import { getDifferentialRecord, getPresentationWorkflow } from "@/lib/differentials";
import { getDifferentialDetailContext, getDifferentialRecord, getPresentationWorkflow } from "@/lib/differentials";
import { isDemoMode, isLocalNoAuthMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { publicAccessContext, shouldResolvePublicCatalogAccess } from "@/lib/public-api-access";
Expand Down Expand Up @@ -63,6 +63,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
if (!record) return notFoundResponse(normalizedSlug);
return differentialResponse({
record,
detailContext: getDifferentialDetailContext(record),
governance: { sourceStatus: governance.source_status, validationStatus: governance.validation_status },
demoMode: true,
});
Expand All @@ -84,6 +85,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
if (!record) return notFoundResponse(normalizedSlug);
return differentialResponse({
record,
detailContext: getDifferentialDetailContext(record),
governance: { sourceStatus: governance.source_status, validationStatus: governance.validation_status },
publicAccess: true,
});
Expand Down Expand Up @@ -118,6 +120,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
if (!record) return notFoundResponse(normalizedSlug);
return differentialResponse({
record,
detailContext: getDifferentialDetailContext(record),
governance: { sourceStatus: governance.source_status, validationStatus: governance.validation_status },
publicAccess: true,
});
Expand Down Expand Up @@ -158,7 +161,27 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
return differentialResponse({ workflow: rowToPresentationWorkflow(row), governance: rowGovernance(row) });
}

return differentialResponse({ record: rowToDifferentialRecord(row), governance: rowGovernance(row) });
// Owner rows can drift from the bundled snapshot, so the catalog-derived
// context ships with the record the client will actually render.
const record = rowToDifferentialRecord(row);
const { data: ownerRowsData, error: ownerRowsError } = await supabase
.from("differential_records")
.select("*")
.eq("owner_id", access.ownerId);
if (ownerRowsError) throw new Error(ownerRowsError.message);
const ownerRows = (ownerRowsData as DifferentialRecordRow[] | null) ?? [];
const ownerRecords = ownerRows.filter((entry) => entry.kind === "diagnosis").map(rowToDifferentialRecord);
const ownerPresentations = ownerRows
.filter((entry) => entry.kind === "presentation")
.map(rowToPresentationWorkflow);
return differentialResponse({
record,
detailContext: getDifferentialDetailContext(record, {
records: ownerRecords,
presentations: ownerPresentations,
}),
governance: rowGovernance(row),
});
} catch (error) {
if (error instanceof AuthenticationError) {
return unauthorizedResponse();
Expand Down
10 changes: 8 additions & 2 deletions src/app/differentials/diagnoses/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";

import { DifferentialDiagnosisPageClient } from "@/components/differentials/differential-diagnosis-page-client";
import { differentialStaticParams, getDifferentialRecord } from "@/lib/differentials";
import { differentialStaticParams, getDifferentialDetailContext, getDifferentialRecord } from "@/lib/differentials";

type DifferentialDiagnosisRouteProps = {
params: Promise<{ slug: string }>;
Expand All @@ -28,5 +28,11 @@ export default async function DifferentialDiagnosisRoute({ params }: Differentia
const record = getDifferentialRecord(slug);
if (!record) notFound();

return <DifferentialDiagnosisPageClient slug={slug} fallbackRecord={record} />;
return (
<DifferentialDiagnosisPageClient
slug={slug}
fallbackRecord={record}
detailContext={getDifferentialDetailContext(record)}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ import { favouriteMatchesCommandScopes } from "@/lib/search-command-surface";
import { appModeIcons } from "@/lib/app-mode-icons";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";

type FavouriteType = "Medication" | "Document" | "Table" | "Saved search" | "Source" | "Service" | "Form";
type FavouriteType =
"Medication" | "Document" | "Table" | "Saved search" | "Source" | "Service" | "Form" | "Differential";
type ViewMode = FavouritesViewMode;
type SortMode = "last-used" | "title" | "type";

Expand Down Expand Up @@ -98,6 +99,8 @@ const typeStyles: Record<FavouriteType, string> = {
Service:
"border-[color:var(--type-service-border)] bg-[color:var(--type-service-soft)] text-[color:var(--type-service)]",
Form: "border-[color:var(--type-form-border)] bg-[color:var(--type-form-soft)] text-[color:var(--type-form)]",
Differential:
"border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]",
};

const lastUsedByItemId: Record<string, string> = {
Expand All @@ -116,6 +119,7 @@ const typeByPrototypeType: Record<PrototypeFavouriteItem["type"], FavouriteType>
sources: "Source",
services: "Service",
forms: "Form",
differentials: "Differential",
};

const fallbackIconByType: Record<PrototypeFavouriteItem["type"], LucideIcon> = {
Expand All @@ -124,6 +128,7 @@ const fallbackIconByType: Record<PrototypeFavouriteItem["type"], LucideIcon> = {
sources: Quote,
services: appModeIcons.services,
forms: FileText,
differentials: appModeIcons.differentials,
};

function lastUsedScore(lastUsed: string): number {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClipboardList, FileText, Folder, LayoutList, Pill, Quote, Search } from "lucide-react";
import { BrainCircuit, ClipboardList, FileText, Folder, LayoutList, Pill, Quote, Search } from "lucide-react";
import { appModeIcons } from "@/lib/app-mode-icons";

export type FavouriteType = "medications" | "documents" | "sources" | "services" | "forms" | "sets";
export type FavouriteType = "medications" | "documents" | "sources" | "services" | "forms" | "differentials" | "sets";
export type FavouriteTabId = "all" | FavouriteType;

export type FavouriteItem = {
Expand Down Expand Up @@ -33,6 +33,7 @@ export const favouriteTabs: Array<{
}> = [
{ id: "all", label: "All", shortLabel: "All", icon: LayoutList },
{ id: "medications", label: "Medications", shortLabel: "Meds", icon: Pill },
{ id: "differentials", label: "Differentials", shortLabel: "Diffs", icon: BrainCircuit },
{ id: "documents", label: "Documents", shortLabel: "Docs", icon: FileText },
{ id: "sources", label: "Sources", shortLabel: "Sources", icon: Quote },
{ id: "services", label: "Services", shortLabel: "Services", icon: appModeIcons.services },
Expand Down
17 changes: 13 additions & 4 deletions src/components/clinical-dashboard/use-differential-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useState } from "react";

import type { DifferentialDetailContext } from "@/lib/differential-detail";
import type { DifferentialSourceStatus, DifferentialValidationStatus } from "@/lib/differential-records";
import type { DifferentialPresentationWorkflow, DifferentialRecord } from "@/lib/differentials";
import { useAuthSession } from "@/lib/supabase/client";
Expand Down Expand Up @@ -29,6 +30,9 @@ export type DifferentialRequestStatus = "loading" | "ready" | "unauthorized" | "
export type DifferentialRecordState = {
status: DifferentialRequestStatus;
record: DifferentialRecord | null;
/** Catalog context computed server-side for the returned record (may lag
* older API deployments, so consumers keep an SSR fallback). */
detailContext: DifferentialDetailContext | null;
demoMode: boolean;
governance: DifferentialRecordGovernance | null;
};
Expand Down Expand Up @@ -118,6 +122,7 @@ export function useDifferentialRecord(slug: string): DifferentialRecordState {
const [state, setState] = useState<DifferentialRecordState>({
status: "loading",
record: null,
detailContext: null,
demoMode: false,
governance: null,
});
Expand All @@ -130,31 +135,35 @@ export function useDifferentialRecord(slug: string): DifferentialRecordState {
if (response.status === 401) {
if (authStatus === "loading") return;
if (authStatus === "authenticated") markSessionExpired();
setState({ status: "unauthorized", record: null, demoMode: false, governance: null });
setState({ status: "unauthorized", record: null, detailContext: null, demoMode: false, governance: null });
return;
}
if (response.status === 404) {
setState({ status: "not_found", record: null, demoMode: false, governance: null });
setState({ status: "not_found", record: null, detailContext: null, demoMode: false, governance: null });
return;
}
if (!response.ok) {
setState({ status: "error", record: null, demoMode: false, governance: null });
setState({ status: "error", record: null, detailContext: null, demoMode: false, governance: null });
return;
}
const payload = (await response.json()) as {
record?: DifferentialRecord;
detailContext?: DifferentialDetailContext;
demoMode?: boolean;
governance?: DifferentialRecordGovernance;
};
setState({
status: payload.record ? "ready" : "not_found",
record: payload.record ?? null,
detailContext: payload.detailContext ?? null,
demoMode: Boolean(payload.demoMode),
governance: payload.governance ?? null,
});
})
.catch(() => {
if (active) setState({ status: "error", record: null, demoMode: false, governance: null });
if (active) {
setState({ status: "error", record: null, detailContext: null, demoMode: false, governance: null });
}
});
return () => {
active = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"use client";

import { ClipboardList } from "lucide-react";
import { BrainCircuit, ClipboardList } from "lucide-react";
import { appModeIcons } from "@/lib/app-mode-icons";
import { useEffect, useMemo, useState } from "react";

import type { FavouriteItem } from "@/components/clinical-dashboard/favourites-prototype-data";
import type { ServiceRecord } from "@/lib/services";
import {
readSavedRegistrySlugs,
savedDifferentialsStorageKey,
savedFormsStorageKey,
savedServicesStorageKey,
subscribeSavedRegistrySlugs,
Expand All @@ -32,11 +33,13 @@ function recordToFavourite(record: ServiceRecord, type: "services" | "forms"): F
export function useSavedRegistryFavourites(): FavouriteItem[] {
const [savedServices, setSavedServices] = useState<string[]>([]);
const [savedForms, setSavedForms] = useState<string[]>([]);
const [savedDifferentials, setSavedDifferentials] = useState<string[]>([]);

useEffect(() => {
const refresh = () => {
setSavedServices(readSavedRegistrySlugs(savedServicesStorageKey));
setSavedForms(readSavedRegistrySlugs(savedFormsStorageKey));
setSavedDifferentials(readSavedRegistrySlugs(savedDifferentialsStorageKey));
};
refresh();
return subscribeSavedRegistrySlugs(refresh);
Expand All @@ -54,6 +57,22 @@ export function useSavedRegistryFavourites(): FavouriteItem[] {
const formItems = forms.records
.filter((record) => savedFormSet.has(record.slug))
.map((record) => recordToFavourite(record, "forms"));
return [...serviceItems, ...formItems];
}, [services.records, forms.records, savedServices, savedForms]);
const differentialItems: FavouriteItem[] = savedDifferentials.map((slug) => ({
id: `differentials:${slug}`,
title: slug
.split("-")
.filter(Boolean)
.map((word) => word[0]?.toUpperCase() + word.slice(1))
.join(" "),
type: "differentials",
set: "Saved differentials",
meta: "Saved diagnosis",
sourceMeta: "Differential",
primaryAction: "Open",
href: `/differentials/diagnoses/${encodeURIComponent(slug)}`,
icon: BrainCircuit,
keywords: slug.replaceAll("-", " "),
}));
return [...serviceItems, ...formItems, ...differentialItems];
}, [services.records, forms.records, savedServices, savedForms, savedDifferentials]);
}
Loading