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
41 changes: 41 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash
# SessionStart hook for Claude Code on the web.
# The app is engine-strict on Node 24.x / npm 11.x, but web containers ship an
# older Node on PATH, so nothing installs or runs until Node 24 is present.
# Installs Node 24 into $HOME/.node24 (cached with the container), exposes it
# via $CLAUDE_ENV_FILE, and installs npm dependencies.
set -euo pipefail

if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi

NODE_VERSION="24.13.0"
NODE_HOME="$HOME/.node24"
NODE_BIN="$NODE_HOME/node-v${NODE_VERSION}-linux-x64/bin"

current_major="$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/' || echo 0)"
if [ "$current_major" != "24" ] && [ ! -x "$NODE_BIN/node" ]; then
echo "[session-start] Installing Node ${NODE_VERSION} (found v${current_major:-none})"
mkdir -p "$NODE_HOME"
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
| tar -xJ -C "$NODE_HOME"
fi

if [ -x "$NODE_BIN/node" ]; then
export PATH="$NODE_BIN:$PATH"
echo "export PATH=\"$NODE_BIN:\$PATH\"" >> "$CLAUDE_ENV_FILE"
fi

echo "[session-start] Using node $(node -v) / npm $(npm -v)"

cd "$CLAUDE_PROJECT_DIR"
# npm ci keeps the lockfile untouched (npm install rewrites peer/optional
# metadata and dirties the worktree); skip entirely when the cached container
# already has node_modules.
if [ ! -d node_modules ]; then
npm ci --no-audit --no-fund
echo "[session-start] Dependencies installed"
else
echo "[session-start] node_modules already present, skipping install"
fi
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
}
]
}
]
}
}
25 changes: 1 addition & 24 deletions data/differentials-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -27730,13 +27730,6 @@
}
],
"presets": [
{
"id": "scenario-presets",
"query": "# Scenario Presets",
"signals": [],
"entryIds": [],
"presentationSlugs": []
},
{
"id": "1-older-adult-acute-confusion",
"query": "older adult acute confusion",
Expand Down Expand Up @@ -27788,14 +27781,6 @@
}
],
"redFlagFlows": [
{
"id": "red-flag-flows",
"title": "# Red Flag Flows",
"entryId": "",
"presentationSlug": "red-flag-flows",
"bedsideQuestions": "",
"keyRedFlags": ""
},
{
"id": "1-suicide",
"title": "1. Suicide",
Expand Down Expand Up @@ -27873,15 +27858,7 @@
"medicine": ["medication", "iatrogenic", "toxicity"],
"qt": ["qtc", "arrhythmia", "toxicity"],
"catatonia": ["mutism", "shutdown", "stupor"],
"field": ["weight"],
"presentation": ["2.6"],
"clinicalhinge": ["2.2"],
"mustnotmiss": ["2.0"],
"immediateactions": ["1.7"],
"mimics": ["1.5"],
"investigations": ["1.2"],
"tags": ["1.1"],
"optiontext": ["1.0"]
"field": ["weight"]
},
"governance": {
"version": "v10",
Expand Down
6 changes: 6 additions & 0 deletions docs/process-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,9 @@ All approved render-surface modules are extracted. `ClinicalDashboard.tsx` went
- **Shipped on `claude/search-cross-mode-links-qscj1n`:** post-answer "Also in your library" strip (`src/lib/cross-mode-links.ts` + `CrossModeLinksStrip`), thread-wide entity fallback, word-boundary field matching in `rankCatalogRecords` (substring hits like "renal" inside "adrenaline" no longer count as name/title matches), `fields=index` slim mode on `/api/medications`, cross-mode click telemetry via `/api/search/interaction` (`crossMode` target, `metadata.interaction: "cross_mode_link_open"`), the same strip on documents-mode results, and answer `crossModes` command-surface parity.
- **Verification debt:** `npm run verify:release` (and its governance/eval gates) has not been run for this workstream — the authoring environment has no live Supabase/OpenAI keys. Run it from a secrets-equipped environment after merge; the cross-mode surface itself is additive/navigational, so `verify:cheap` + `verify:ui` are the load-bearing local gates.
- **Telemetry note:** cross-mode clicks write `rag_query_misses` rows with `clicked_document_id: null` and the target mode/slug in `metadata`; retrieval-quality reviews that aggregate misses by document should filter on `metadata.interaction`.

## Answer-thread Back button: URL and visible answer can disagree (2026-07-06)

- **Behaviour:** inside an Answer thread, browser Back changes the URL (`/?mode=answer&q=A&run=1` ← `...q=B&run=1`) but the rendered answer/thread does not change. Two guards produce this: the auto-run effect skips when `run=1` is already present, and the answer view early-returns when an answer is already on screen (`ClinicalDashboard`). This is thread persistence by design, not an accident — clearing the thread on Back would destroy in-progress clinical context.
- **Open product decision:** either (a) accept that the URL is not a faithful pointer to the visible answer inside a thread (current state), or (b) make Back restore the previous question's answer from thread history. Option (b) needs answer-thread state keyed by URL and re-render on `popstate`/param change; it is a deliberate feature, not a bug fix.
- **Guardrail until decided:** do not "fix" the skipped auto-run on Back in isolation — re-running the search on Back would clobber the visible thread with a regenerated (and possibly different) answer, which is worse than either deliberate option.
16 changes: 13 additions & 3 deletions scripts/lib/parse-differentials-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,16 @@ function mergeDiagnosisRecords(existing: DifferentialRecord, incoming: Different
};
}

export function parseScenarioPresets(markdown: string): DifferentialScenarioPreset[] {
// Splitting on "## " leaves any document preamble (the top-level "# Title"
// heading and intro prose) as a phantom first section; it must be dropped or
// it becomes a bogus record (e.g. a "# Scenario Presets" preset).
function markdownSections(markdown: string) {
const sections = markdown.split(/(?:^|\n)##\s+/).filter(Boolean);
return markdown.trimStart().startsWith("## ") ? sections : sections.slice(1);
}

export function parseScenarioPresets(markdown: string): DifferentialScenarioPreset[] {
const sections = markdownSections(markdown);
return sections.map((section, index) => {
const lines = section.split("\n");
const titleLine = lines[0]?.trim() ?? `Preset ${index + 1}`;
Expand Down Expand Up @@ -430,7 +438,7 @@ export function parseScenarioPresets(markdown: string): DifferentialScenarioPres
}

export function parseRedFlagFlows(markdown: string): DifferentialRedFlagFlow[] {
const sections = markdown.split(/(?:^|\n)##\s+/).filter(Boolean);
const sections = markdownSections(markdown);
return sections.map((section, index) => {
const title = section.split("\n")[0]?.trim() ?? `Flow ${index + 1}`;
const entryId = section.match(/Entry\s+(\d+[A-Z]?)/i)?.[1]?.toUpperCase() ?? "";
Expand Down Expand Up @@ -463,7 +471,9 @@ export function parseSearchAliases(markdown: string): Record<string, string[]> {
const values = match[2]
.split(",")
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
// The export also carries field-weight tables (e.g. "| tags | 1.1 |");
// bare numbers are ranking weights, not clinical synonyms.
.filter((item) => Boolean(item) && !/^\d+(\.\d+)?$/.test(item));
if (token && values.length) aliases[token] = values;
}
return aliases;
Expand Down
1 change: 0 additions & 1 deletion src/app/api/medications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
medicationValidationStatus,
rowGovernance,
rowToMedicationRecord,
type MedicationRecordRow,
} from "@/lib/medication-records";
import {
medicationToSearchResult,
Expand Down
1 change: 0 additions & 1 deletion src/app/api/registry/records/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
rowGovernance,
rowToServiceRecord,
type RegistryRecordKind,
type RegistryRecordRow,
} from "@/lib/registry-records";
import { fetchOwnerRegistryRowsWithSeed } from "@/lib/registry-seed";
import { rankServiceRecords, serviceRecords, type ServiceRecord, type ServiceSearchMatch } from "@/lib/services";
Expand Down
4 changes: 3 additions & 1 deletion src/app/favourites/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ export default async function FavouritesPage({ searchParams }: FavouritesPagePro
const params = searchParams ? await searchParams : {};
const query = firstSearchParam(params.q)?.trim() ?? "";

return <FavouritesCommandLibraryPage key={query} query={query} />;
// No key={query} remount: query is a pure prop, and remounting on query
// change wiped the set/type/view/sort selections when clearing a search.
return <FavouritesCommandLibraryPage query={query} />;
}
1 change: 0 additions & 1 deletion src/components/ClinicalDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2488,7 +2488,7 @@
urlDocumentSearchBootstrappedRef.current = true;
void executeSearch(searchText, mode, scopeFilters);
// URL search intentionally runs once when the selected mode can execute.
}, [canRunSearch, answerThreadBootstrapped]);

Check warning on line 2491 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions / verify

React Hook useEffect has missing dependencies: 'executeSearch' and 'scopeFilters'. Either include them or remove the dependency array

useEffect(() => {
const updateHash = () => {
Expand Down Expand Up @@ -4129,7 +4129,6 @@
) : null
) : showAnswerHome ? (
<AnswerEmptyState
onPickSample={setQuery}
onSearchDocuments={() => setSearchMode("documents")}
onUploadDocument={openUploadDrawer}
desktopComposerSlotId={desktopHomeComposerSlotId}
Expand Down
1 change: 1 addition & 0 deletions src/components/DocumentViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2166,6 +2166,7 @@ export function DocumentViewer({
}, [
authStatus,
authorizationHeader,
canUsePrivateApis,
canViewSourceDocuments,
clientDemoMode,
documentId,
Expand Down
2 changes: 0 additions & 2 deletions src/components/clinical-dashboard/answer-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ export function CopyButton({
}

export function AnswerEmptyState({
onPickSample: _onPickSample,
onSearchDocuments,
onUploadDocument,
desktopComposerSlotId,
}: {
onPickSample: (sample: string) => void;
onSearchDocuments: () => void;
onUploadDocument: () => void;
desktopComposerSlotId?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,13 +645,19 @@ function FavouritesTable({
})}
{tableRows.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-10 text-center">
<Search className="mx-auto mb-2 h-5 w-5 text-[color:var(--text-soft)]" aria-hidden />
<p className="font-black text-[color:var(--text-heading)]">No favourites match</p>
<p className="mt-1 text-sm font-semibold text-[color:var(--text-muted)]">
Clear filters or search to show saved clinical work.
</p>
</td>
{/* The Evidence column is hidden below lg, so span 5 there and 6 at lg+. */}
{[
{ colSpan: 5, className: "px-4 py-10 text-center lg:hidden" },
{ colSpan: 6, className: "hidden px-4 py-10 text-center lg:table-cell" },
].map(({ colSpan, className }) => (
<td key={colSpan} colSpan={colSpan} className={className}>
<Search className="mx-auto mb-2 h-5 w-5 text-[color:var(--text-soft)]" aria-hidden />
<p className="font-black text-[color:var(--text-heading)]">No favourites match</p>
<p className="mt-1 text-sm font-semibold text-[color:var(--text-muted)]">
Clear filters or search to show saved clinical work.
</p>
</td>
))}
</tr>
) : null}
</tbody>
Expand Down
Loading