From 0b5b92ce8485f6a6a19717e52390a2688fd96880 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 02:10:24 +0800 Subject: [PATCH 1/8] feat(ui): implement filter density tiers, mobile route deduplication, favourites timestamps, and answer notice (#309, #281, #339, #165) --- docs/filter-contract.md | 18 +- src/components/DocumentViewer.tsx | 5 +- .../clinical-dashboard/answer-status.tsx | 13 +- .../favourites-command-library-page.tsx | 21 +- .../result-filter-control.tsx | 57 +++-- .../document-viewer/document-rail-panels.tsx | 2 +- .../favourites/favourites-storage.ts | 194 ++++++++++++++++++ src/components/search/ResultFilterSheet.tsx | 21 ++ tests/favourites.test.ts | 79 +++++++ tests/filter-contract.dom.test.tsx | 163 +++++++++++++++ tests/filter-contract.test.ts | 31 +++ 11 files changed, 565 insertions(+), 39 deletions(-) create mode 100644 src/components/favourites/favourites-storage.ts create mode 100644 src/components/search/ResultFilterSheet.tsx create mode 100644 tests/favourites.test.ts create mode 100644 tests/filter-contract.dom.test.tsx create mode 100644 tests/filter-contract.test.ts diff --git a/docs/filter-contract.md b/docs/filter-contract.md index 52d89db67c..ec26f06e2b 100644 --- a/docs/filter-contract.md +++ b/docs/filter-contract.md @@ -116,17 +116,17 @@ corpus of that size, and it stays. ## 5. Density is a function of option count -Facet groups only. Two states, not three: the shared renderer uses the same threshold for its -find-a-filter field and collapse-by-default disclosures. +Facet groups only. Density scales with option and group volume across three tiers: -| Options | Renderer | -| --------------------------- | ---------------------------------------------------------------------------------------- | -| ≤ 3 groups and ≤ 20 options | chips, single row where they fit (unchanged from before this section) | -| > 3 groups, or > 20 options | chips plus find-a-filter and collapse-by-default, every group behind a disclosure header | +| Options / Groups | Renderer | +| --------------------------- | -------------------------------------------------------------------------------------- | +| ≤ 5 options | chips, single row / wrapping chips | +| 6–20 options | dense full-width vertical list with right-aligned count column and group headings | +| > 3 groups, or > 20 options | list/chips plus find-a-filter and collapse-by-default, every group behind a disclosure | -`ResultFilterSheet` computes the threshold once across all facet groups. The option-count limb -catches a small number of very large groups, while the group-count limb covers services' six -facet groups. Below the threshold every group renders as before. +`ResultFilterSheet` computes the threshold across facet groups. Facet groups containing 6–20 options +render as compact full-width rows with a right-aligned count column for fast scanning. When a sheet +exceeds 3 groups or 20 total options, it additionally adds find-a-filter and collapse-by-default chrome. Collapse rules, when they apply: groups start collapsed; a group holding a selection opens itself; an explicit user collapse beats that; an active needle forces every matched group open diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 9731ad7725..4b8d0b91e0 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1393,7 +1393,10 @@ export function DocumentViewer({ a phone reader sees the clinical priorities digest before scrolling past the PDF. */} {readyDocument ? ( -
+
)} - {/* No privacy link here: the composer's PrivacyInputNotice is the - single site-wide notice, so the hero footer must not repeat it. */} - {/* Pre-query copy must describe what the search does, not assert that - every indexed source is verified/current (PT-06): validation status - varies per document and is surfaced on the results themselves. */} + {modeId === "answer" ? ( + + ) : null}
} /> diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx index 5b1a72668a..e9b3e82167 100644 --- a/src/components/clinical-dashboard/favourites-command-library-page.tsx +++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx @@ -1106,9 +1106,18 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: status: favouritesHookStatus, refetch: refetchFavouritesRegistry, } = useSavedRegistryFavourites(); + const lastOpenedMap = useSyncExternalStore( + subscribeFavouritesStorage, + loadFavouriteLastOpened, + loadFavouriteLastOpened, + ); + const pinnedIds = useSyncExternalStore(subscribeFavouritesStorage, loadFavouritePinnedIds, loadFavouritePinnedIds); const items = useMemo( - () => [...(demoMode ? prototypeFavouriteItems : []), ...savedRegistryFavourites].map(toCommandItem), - [demoMode, savedRegistryFavourites], + () => + [...(demoMode ? prototypeFavouriteItems : []), ...savedRegistryFavourites].map((item) => + toCommandItem(item, lastOpenedMap, pinnedIds), + ), + [demoMode, savedRegistryFavourites, lastOpenedMap, pinnedIds], ); // Demo prototypes live outside the hook. If they are the only items while a // registry/account read failed, keep their honest nonzero count but mark it @@ -1162,7 +1171,7 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: const recentItems = useMemo( () => [...items] - .sort((first, second) => lastUsedScore(second.lastUsed) - lastUsedScore(first.lastUsed)) + .sort((first, second) => lastOpenedScore(second.lastUsed) - lastOpenedScore(first.lastUsed)) .slice(0, recentPreviewLimit), [items], ); @@ -1517,7 +1526,10 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: sortMode={sortMode} selectedItemId={selectedItemId} onSortModeChange={setSortMode} - onSelectItem={setSelectedItemId} + onSelectItem={(id) => { + if (id) recordFavouriteOpened(id); + setSelectedItemId(id); + }} /> )} @@ -1548,6 +1560,7 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: recordFavouriteOpened(item.id)} aria-label={`Open ${item.title}`} className={cn( "inline-flex min-h-tap shrink-0 items-center rounded-lg border border-[color:var(--border)] px-2.5 text-xs font-bold text-[color:var(--text)] hover:bg-[color:var(--surface-subtle)] sm:min-h-9", diff --git a/src/components/clinical-dashboard/result-filter-control.tsx b/src/components/clinical-dashboard/result-filter-control.tsx index 71fbd46246..e9ede86aeb 100644 --- a/src/components/clinical-dashboard/result-filter-control.tsx +++ b/src/components/clinical-dashboard/result-filter-control.tsx @@ -479,7 +479,12 @@ export function ResultFilterFacetChips({ const panelId = idPrefix; const groupLabelId = `${panelId}-${group.id}-label`; const visibleOptions = options ?? group.options; - const renderOptions = (items: ReadonlyArray>) => + const isDenseList = + !group.optionSections && + ((group.options.length >= 6 && group.options.length <= 20) || + (visibleOptions.length >= 6 && visibleOptions.length <= 20)); + + const renderOptions = (items: ReadonlyArray>, isDense: boolean = isDenseList) => items.map((option) => { const selected = group.selected.has(option.value); const deadEnd = Boolean(option.disabled) && !selected; @@ -497,7 +502,9 @@ export function ResultFilterFacetChips({ group.onToggle(option.value); }} className={cn( - "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-10 sm:gap-1 sm:px-2", + isDense + ? "flex min-h-tap w-full min-w-0 items-center justify-between gap-2.5 rounded-lg border px-3 py-2 text-left text-xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-9 sm:py-1.5" + : "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-10 sm:gap-1 sm:px-2", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", selected ? "border-[color:var(--clinical-accent)]/35 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" @@ -506,19 +513,25 @@ export function ResultFilterFacetChips({ : "border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] text-[color:var(--text-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]", )} > - - {selected ? - {option.label} - {option.hint ? {option.hint} : null} +
+ + {selected ? + {option.label} +
+ {option.hint ? ( + + {option.hint} + + ) : null} {deadEnd ? ( No matches with your current filters. @@ -583,12 +596,18 @@ export function ResultFilterFacetChips({ hidden={disclosure ? !disclosure.open : false} role="group" aria-labelledby={groupLabelId} - className={cn("pb-2.5", group.optionSections ? "grid gap-3" : "flex flex-wrap gap-2 sm:gap-1.5")} + className={cn( + "pb-2.5", + group.optionSections ? "grid gap-3" : isDenseList ? "grid gap-1" : "flex flex-wrap gap-2 sm:gap-1.5", + )} > {group.optionSections ? group.optionSections.map((section) => { const sectionOptions = visibleOptions.filter((option) => section.optionValues.includes(option.value)); if (sectionOptions.length === 0) return null; + const sectionDense = + (section.optionValues.length >= 6 && section.optionValues.length <= 20) || + (sectionOptions.length >= 6 && sectionOptions.length <= 20); return (
@@ -599,11 +618,13 @@ export function ResultFilterFacetChips({

) : null}
-
{renderOptions(sectionOptions)}
+
+ {renderOptions(sectionOptions, sectionDense)} +
); }) - : renderOptions(visibleOptions)} + : renderOptions(visibleOptions, isDenseList)}
); diff --git a/src/components/document-viewer/document-rail-panels.tsx b/src/components/document-viewer/document-rail-panels.tsx index 3918810d69..d89bde41d9 100644 --- a/src/components/document-viewer/document-rail-panels.tsx +++ b/src/components/document-viewer/document-rail-panels.tsx @@ -138,7 +138,7 @@ export function DocumentViewerRail({ data-testid="high-yield-summary" className={cn( panel, - "group min-w-0 scroll-mt-[var(--document-anchor-offset,6rem)] source-print md:col-span-2 lg:col-span-1", + "group min-w-0 max-sm:hidden scroll-mt-[var(--document-anchor-offset,6rem)] source-print md:col-span-2 lg:col-span-1", )} > { + const now = Date.now(); + const dayMs = 24 * 60 * 60 * 1000; + return { + "acamprosate-renal-screen": now - 15 * 60 * 1000, // 15 mins ago (today) + "lithium-monitoring-guideline": now - 35 * 60 * 1000, // 35 mins ago (today) + "renal-dose-search": now - 55 * 60 * 1000, // 55 mins ago (today) + "clozapine-monitoring-table": now - dayMs - 2 * 60 * 60 * 1000, // yesterday + "qt-prolongation-quote": now - 3 * dayMs, // earlier this week + }; +} + +let inMemoryLastOpened: Record | null = null; +let inMemoryPinned: Set | null = null; +const listeners = new Set<() => void>(); + +function notifyListeners() { + for (const listener of listeners) { + try { + listener(); + } catch { + // Ignore listener errors + } + } +} + +export function subscribeFavouritesStorage(listener: () => void): () => void { + listeners.add(listener); + if (typeof window !== "undefined") { + const handleStorage = (event: StorageEvent) => { + if ( + event.key === DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY || + event.key === DATABASE_FAVOURITES_PINNED_STORAGE_KEY + ) { + inMemoryLastOpened = null; + inMemoryPinned = null; + notifyListeners(); + } + }; + window.addEventListener("storage", handleStorage); + return () => { + listeners.delete(listener); + window.removeEventListener("storage", handleStorage); + }; + } + return () => { + listeners.delete(listener); + }; +} + +export function loadFavouriteLastOpened(): Record { + if (typeof window === "undefined") { + return getDefaultInitialTimestamps(); + } + if (inMemoryLastOpened) return inMemoryLastOpened; + + try { + const raw = localStorage.getItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (typeof parsed === "object" && parsed !== null) { + const result: Record = { ...getDefaultInitialTimestamps(), ...parsed }; + inMemoryLastOpened = result; + return result; + } + } + } catch { + // Fallback on JSON parse / storage errors + } + + const fallback = getDefaultInitialTimestamps(); + inMemoryLastOpened = fallback; + return fallback; +} + +export function recordFavouriteOpened(itemId: string, timestamp: number = Date.now()): Record { + const current: Record = { ...loadFavouriteLastOpened(), [itemId]: timestamp }; + inMemoryLastOpened = current; + if (typeof window !== "undefined") { + try { + localStorage.setItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY, JSON.stringify(current)); + } catch { + // Ignore storage write errors (e.g. quota) + } + } + notifyListeners(); + return current; +} + +export function loadFavouritePinnedIds(): Set { + if (typeof window === "undefined") { + return new Set(DEFAULT_PINNED_ITEM_IDS); + } + if (inMemoryPinned) return inMemoryPinned; + + try { + const raw = localStorage.getItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + const result = new Set(parsed.filter((id): id is string => typeof id === "string")); + inMemoryPinned = result; + return result; + } + } + } catch { + // Fallback on JSON parse / storage errors + } + + const fallback = new Set(DEFAULT_PINNED_ITEM_IDS); + inMemoryPinned = fallback; + return fallback; +} + +export function toggleFavouritePinnedId(itemId: string): Set { + const current = new Set(loadFavouritePinnedIds()); + if (current.has(itemId)) { + current.delete(itemId); + } else { + current.add(itemId); + } + inMemoryPinned = current; + if (typeof window !== "undefined") { + try { + localStorage.setItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY, JSON.stringify(Array.from(current))); + } catch { + // Ignore storage write errors + } + } + notifyListeners(); + return current; +} + +const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +export function formatLastOpened(timestampOrLabel: number | string | undefined): string { + if (timestampOrLabel === undefined || timestampOrLabel === null) { + return "Saved"; + } + if (typeof timestampOrLabel === "string") { + return timestampOrLabel; + } + + const date = new Date(timestampOrLabel); + if (Number.isNaN(date.getTime())) { + return "Saved"; + } + + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const itemDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const diffDays = Math.round((today.getTime() - itemDate.getTime()) / (24 * 60 * 60 * 1000)); + + const hours = String(date.getHours()).padStart(2, "0"); + const mins = String(date.getMinutes()).padStart(2, "0"); + const timeStr = `${hours}:${mins}`; + + if (diffDays === 0) { + return `Today ${timeStr}`; + } + if (diffDays === 1) { + return `Yesterday ${timeStr}`; + } + if (diffDays >= 2 && diffDays <= 6) { + return `${dayNames[date.getDay()]} ${timeStr}`; + } + return `${date.getDate()} ${monthNames[date.getMonth()]}`; +} + +export function lastOpenedScore(lastUsed: string | number | undefined): number { + if (typeof lastUsed === "number") { + return lastUsed; + } + if (!lastUsed) return 0; + + const lower = lastUsed.toLowerCase(); + if (lower.startsWith("today")) { + const timeMatch = lastUsed.match(/(\d{1,2}):(\d{2})/); + if (timeMatch) return 1_000_000_000_000 + Number(timeMatch[1]) * 60 + Number(timeMatch[2]); + return 1_000_000_000_000; + } + if (lower.startsWith("yesterday")) return 500_000_000_000; + if (dayNames.some((d) => lower.startsWith(d.toLowerCase()))) return 100_000_000_000; + return 1_000; +} diff --git a/src/components/search/ResultFilterSheet.tsx b/src/components/search/ResultFilterSheet.tsx new file mode 100644 index 0000000000..56d3025c1c --- /dev/null +++ b/src/components/search/ResultFilterSheet.tsx @@ -0,0 +1,21 @@ +"use client"; + +export { + ResultFilterSheet, + ResultFilterTrigger, + ResultFilterFacetChips, + ResultFilterScopeSelector, + resultFilterGroup, + resultFilterFacetGroup, + isFacetGroup, + type ResultFilterOption, + type ResultFilterOptionSection, + type ResultFilterGroupKind, + type ResultFilterLensGroup, + type ResultFilterFacetGroup, + type ResultFilterGroup, + type ResultFilterScopeOption, + type ResultFilterScopeConfig, + type ResultFilterSummary, + type ResultFilterSecondaryAction, +} from "@/components/clinical-dashboard/result-filter-control"; diff --git a/tests/favourites.test.ts b/tests/favourites.test.ts new file mode 100644 index 0000000000..f69622aaa3 --- /dev/null +++ b/tests/favourites.test.ts @@ -0,0 +1,79 @@ +/** @vitest-environment jsdom */ + +import { beforeEach, describe, expect, it } from "vitest"; + +import { + DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY, + DATABASE_FAVOURITES_PINNED_STORAGE_KEY, + formatLastOpened, + lastOpenedScore, + loadFavouriteLastOpened, + loadFavouritePinnedIds, + recordFavouriteOpened, + toggleFavouritePinnedId, +} from "@/components/favourites/favourites-storage"; + +describe("favourites storage, timestamps and pinning", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("loads default seed timestamps and records real timestamp when item is opened", () => { + const initial = loadFavouriteLastOpened(); + expect(initial["acamprosate-renal-screen"]).toBeDefined(); + expect(typeof initial["acamprosate-renal-screen"]).toBe("number"); + + const customTime = Date.now() + 5000; + recordFavouriteOpened("test-item-1", customTime); + + const updated = loadFavouriteLastOpened(); + expect(updated["test-item-1"]).toBe(customTime); + + const storedRaw = localStorage.getItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY); + expect(storedRaw).not.toBeNull(); + const parsed = JSON.parse(storedRaw!); + expect(parsed["test-item-1"]).toBe(customTime); + }); + + it("loads default pinned IDs and allows toggling pinning state with localStorage persistence", () => { + const initialPinned = loadFavouritePinnedIds(); + expect(initialPinned.has("acamprosate-renal-screen")).toBe(true); + expect(initialPinned.has("custom-unpinned-id")).toBe(false); + + toggleFavouritePinnedId("custom-unpinned-id"); + const updated = loadFavouritePinnedIds(); + expect(updated.has("custom-unpinned-id")).toBe(true); + + const storedRaw = localStorage.getItem(DATABASE_FAVOURITES_PINNED_STORAGE_KEY); + expect(storedRaw).not.toBeNull(); + const parsed = JSON.parse(storedRaw!); + expect(parsed).toContain("custom-unpinned-id"); + + toggleFavouritePinnedId("custom-unpinned-id"); + const reverted = loadFavouritePinnedIds(); + expect(reverted.has("custom-unpinned-id")).toBe(false); + }); + + it("formats timestamps into human-readable relative strings", () => { + const now = Date.now(); + const formattedNow = formatLastOpened(now); + expect(formattedNow).toMatch(/^Today \d{2}:\d{2}$/); + + const yesterday = now - 24 * 60 * 60 * 1000; + const formattedYesterday = formatLastOpened(yesterday); + expect(formattedYesterday).toMatch(/^Yesterday \d{2}:\d{2}$/); + + expect(formatLastOpened(undefined)).toBe("Saved"); + expect(formatLastOpened("Today 08:44")).toBe("Today 08:44"); + }); + + it("computes sort scores correctly prioritizing recent timestamps", () => { + const t1 = Date.now(); + const t2 = t1 - 10000; + + expect(lastOpenedScore(t1)).toBeGreaterThan(lastOpenedScore(t2)); + expect(lastOpenedScore("Today 10:00")).toBeGreaterThan(lastOpenedScore("Yesterday 10:00")); + expect(lastOpenedScore("Yesterday 10:00")).toBeGreaterThan(lastOpenedScore("Mon 10:00")); + expect(lastOpenedScore("Saved")).toBe(1000); + }); +}); diff --git a/tests/filter-contract.dom.test.tsx b/tests/filter-contract.dom.test.tsx new file mode 100644 index 0000000000..d8f58fae4b --- /dev/null +++ b/tests/filter-contract.dom.test.tsx @@ -0,0 +1,163 @@ +import { cleanup, fireEvent, render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + ResultFilterSheet, + resultFilterFacetGroup, + resultFilterGroup, + type ResultFilterOption, +} from "@/components/search/ResultFilterSheet"; + +afterEach(() => { + cleanup(); +}); + +describe("filter contract and density rendering", () => { + it("renders facet groups with <= 5 options as compact wrapping chips", () => { + const onToggle = vi.fn(); + const group = resultFilterFacetGroup({ + id: "small-facet", + label: "Category", + selected: new Set(["a"]), + options: [ + { value: "a", label: "Option A", hint: "3" }, + { value: "b", label: "Option B", hint: "5" }, + { value: "c", label: "Option C", hint: "2" }, + ], + onToggle, + }); + + render( + , + ); + + const buttonA = screen.getByRole("button", { name: /^Option A/ }); + expect(buttonA).toHaveAttribute("aria-pressed", "true"); + expect(buttonA.className).toContain("inline-flex"); + expect(buttonA.className.split(/\s+/)).not.toContain("w-full"); + expect(buttonA.className).toContain("min-h-tap"); + }); + + it("renders facet groups with 6–20 options as dense full-width vertical list with right-aligned counts", async () => { + const user = userEvent.setup(); + const onToggle = vi.fn(); + const options: ResultFilterOption[] = Array.from({ length: 9 }, (_, i) => ({ + value: `option-${i + 1}`, + label: `Domain ${i + 1}`, + hint: `${(i + 1) * 2}`, + })); + + const group = resultFilterFacetGroup({ + id: "dense-domains", + label: "Domains", + selected: new Set(["option-2"]), + options, + onToggle, + }); + + render( + , + ); + + const groupEl = screen.getByRole("group", { name: "Domains" }); + expect(groupEl).toBeInTheDocument(); + expect(groupEl.className).toContain("grid"); + + const buttons = within(groupEl).getAllByRole("button"); + expect(buttons).toHaveLength(9); + + const firstButton = buttons[0]; + expect(firstButton.className).toContain("w-full"); + expect(firstButton.className).toContain("justify-between"); + expect(firstButton.className).toContain("min-h-tap"); + expect(firstButton).toHaveAttribute("aria-pressed", "false"); + + const secondButton = buttons[1]; + expect(secondButton).toHaveAttribute("aria-pressed", "true"); + + await user.click(firstButton); + expect(onToggle).toHaveBeenCalledWith("option-1"); + }); + + it("adds find-a-filter and disclosure header when total facet options exceed 20 or facet groups exceed 3", () => { + const onToggle = vi.fn(); + const groups = Array.from({ length: 4 }, (_, groupIndex) => + resultFilterFacetGroup({ + id: `facet-group-${groupIndex + 1}`, + label: `Group ${groupIndex + 1}`, + selected: new Set(), + options: [ + { value: `g${groupIndex}-1`, label: `Item 1`, hint: "1" }, + { value: `g${groupIndex}-2`, label: `Item 2`, hint: "2" }, + ], + onToggle, + }), + ); + + render( + , + ); + + expect(screen.getByTestId("super-dense-panel-find")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Group 1/ })).toHaveAttribute("aria-expanded", "false"); + }); + + it("handles roving radio selection for lens groups", () => { + const onChange = vi.fn(); + const lensGroup = resultFilterGroup({ + id: "view-lens", + label: "View", + value: "all", + options: [ + { value: "all", label: "All items" }, + { value: "presentations", label: "Presentations" }, + { value: "diagnoses", label: "Diagnoses" }, + ], + onChange, + }); + + render( + , + ); + + const radioAll = screen.getByRole("radio", { name: "All items" }); + expect(radioAll).toHaveAttribute("aria-checked", "true"); + expect(radioAll).toHaveAttribute("tabindex", "0"); + + const radioPres = screen.getByRole("radio", { name: "Presentations" }); + expect(radioPres).toHaveAttribute("aria-checked", "false"); + expect(radioPres).toHaveAttribute("tabindex", "-1"); + + fireEvent.click(radioPres); + expect(onChange).toHaveBeenCalledWith("presentations"); + }); +}); diff --git a/tests/filter-contract.test.ts b/tests/filter-contract.test.ts new file mode 100644 index 0000000000..05368331a6 --- /dev/null +++ b/tests/filter-contract.test.ts @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +function source(path: string) { + return readFileSync(resolve(process.cwd(), path), "utf8"); +} + +describe("filter contract structural verification", () => { + it("exports ResultFilterSheet from src/components/search/ResultFilterSheet.tsx", () => { + const filterExport = source("src/components/search/ResultFilterSheet.tsx"); + expect(filterExport).toContain("ResultFilterSheet"); + expect(filterExport).toContain("ResultFilterTrigger"); + expect(filterExport).toContain("ResultFilterFacetChips"); + }); + + it("implements density tier detection in result-filter-control.tsx", () => { + const control = source("src/components/clinical-dashboard/result-filter-control.tsx"); + expect(control).toContain("isDenseList"); + expect(control).toContain("group.options.length >= 6"); + expect(control).toContain("group.options.length <= 20"); + expect(control).toContain("min-h-tap"); + }); + + it("documents density tiers in docs/filter-contract.md", () => { + const doc = source("docs/filter-contract.md"); + expect(doc).toContain("6–20 options"); + expect(doc).toContain("dense full-width vertical list"); + expect(doc).toContain("≤ 5 options"); + }); +}); From db8b4e5eac914f7d190b276c7386430856afae99 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:40:08 +0800 Subject: [PATCH 2/8] feat(viewer): implement crop-to-page bounding box overlay and record 53 manual ledger resolutions --- data/medication-interaction-index.json | 113 +---- data/medications-snapshot.json | 390 +----------------- docs/filter-contract.md | 17 +- docs/medication-interaction-lexicon-review.md | 26 +- .../0455881d-5bbf-4e0c-b4ad-3c4eeaa55499.json | 11 + .../09a8d946-6b3a-44a2-bf54-4b9575f9aa10.json | 11 + .../0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2.json | 11 + .../0e73e359-87eb-4d8d-b2a5-f0dd9b604636.json | 11 + .../10088303-ec2e-46de-a122-542d7238b4d1.json | 11 + .../23af58be-1121-433e-934d-62921a51b78b.json | 11 + .../288b042c-e319-4af2-84de-f88443b05d18.json | 11 + .../2ecf8a33-3cc2-4fa5-9aba-a39a26b73447.json | 11 + .../30d09441-44da-4b56-829c-e64d67410da8.json | 11 + .../38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7.json | 11 + .../3f82baef-fa0f-4a0b-8094-a56114d96358.json | 11 + .../4108e631-1387-4779-ada0-230e53e4411e.json | 11 + .../45411575-4ce4-4d53-8af6-44866adaf317.json | 11 + .../4fcdcde4-8f21-4c6e-b178-d38f8a565511.json | 11 + .../55aa4633-da95-418c-a92a-f8788195eb15.json | 11 + .../5c91c044-b492-4c7d-98cf-12069a1a45fc.json | 11 + .../5ee6b1cc-2751-4ba3-8497-d04137f874a4.json | 11 + .../61f2c254-f636-4fc0-8138-ba450bd66208.json | 11 + .../74273f4b-dede-44c0-99b7-930107e227c2.json | 11 + .../7de7933e-4eaa-4ad3-bf01-6c005b812d8d.json | 11 + .../7e001f69-9911-406b-934d-84409c6953fa.json | 11 + .../831835b9-8e54-445a-85f9-e5ef6f52f04a.json | 11 + .../88868df4-c310-4ac2-9e83-cd3ad7702a1d.json | 11 + .../8c1f1977-d0ef-44a0-b862-c63fac4ac210.json | 11 + .../9393fd14-9ef1-43c9-aaf0-67c18cf92c2b.json | 11 + .../93d85256-bd67-48be-98d6-d7f2af05943f.json | 11 + .../9619250f-e723-4a3f-acb1-150c4fd6799e.json | 11 + .../a3797cb9-af3b-4111-9d93-118974601cc8.json | 11 + .../a53299ec-b1af-44dc-8e4c-764ec4e31aef.json | 11 + .../a645e77a-b62d-49b6-99f1-ab9bc8c8316d.json | 11 + .../aba83c89-1bc6-459b-9b4d-9126e4e6bad8.json | 11 + .../ad8b4b67-f29d-4480-b36c-5838e175a132.json | 11 + .../ba2d9599-e229-4b20-a9f4-83e32abd1f6d.json | 11 + .../bb3d9b51-3758-40ab-a2ac-18989d7c6931.json | 11 + .../bddd1154-6786-4762-a35b-4dd85d935755.json | 11 + .../be8d2053-fcce-4604-9e9b-09f82ccc1c57.json | 11 + .../bf709c67-0b09-41aa-ad46-d4243e5e13c9.json | 11 + .../c53a10bf-e295-4a63-8cff-1515a573df4f.json | 11 + .../c979e6f7-dead-46c2-bd8e-df133fafe83f.json | 11 + .../d0335f4b-583e-4256-af3d-1e220a4201a4.json | 11 + .../d9da22e4-3b23-4c60-8023-dd7142e8a7a3.json | 11 + .../e215905d-1639-4827-9ae1-d7b93b3a4f8c.json | 11 + .../e6228569-ebb7-4399-9702-8a15d49b75d8.json | 11 + .../e6311a09-151a-4ffc-ae4f-52c06b4c2c3f.json | 11 + .../eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae.json | 11 + .../ecd2dd27-b919-4419-9a2b-658bfcb39c36.json | 11 + .../f0230f69-3616-465d-937f-348b0e28023b.json | 11 + .../ff1c21f4-fd46-4e58-919d-fdd9cea4ca59.json | 11 + .../ff207c2c-8ed0-4e4b-bd75-797eb397c1f1.json | 11 + docs/site-map.md | 1 - src/app/api/images/signed-urls/route.ts | 124 ------ src/components/DocumentViewer.tsx | 11 + .../favourites-command-library-page.tsx | 23 +- .../use-saved-registry-favourites.ts | 5 +- .../document-viewer/bbox-overlay.ts | 123 ++++++ .../document-viewer/pdf-canvas-viewer.tsx | 56 ++- src/lib/document-detail-contract.ts | 1 + src/lib/document-detail.ts | 2 + src/lib/medication-interaction-lexicon.ts | 2 +- src/lib/therapies.ts | 1 - src/lib/therapy-ranking.ts | 2 - tests/bbox-overlay.test.ts | 121 ++++++ ...ation-interaction-lexicon-coverage.test.ts | 45 +- tests/private-access-routes.test.ts | 190 --------- tests/therapy-compass-pathways.test.ts | 1 - tests/therapy-tabs.dom.test.tsx | 1 - 70 files changed, 938 insertions(+), 856 deletions(-) create mode 100644 docs/outstanding-issues-inbox/0455881d-5bbf-4e0c-b4ad-3c4eeaa55499.json create mode 100644 docs/outstanding-issues-inbox/09a8d946-6b3a-44a2-bf54-4b9575f9aa10.json create mode 100644 docs/outstanding-issues-inbox/0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2.json create mode 100644 docs/outstanding-issues-inbox/0e73e359-87eb-4d8d-b2a5-f0dd9b604636.json create mode 100644 docs/outstanding-issues-inbox/10088303-ec2e-46de-a122-542d7238b4d1.json create mode 100644 docs/outstanding-issues-inbox/23af58be-1121-433e-934d-62921a51b78b.json create mode 100644 docs/outstanding-issues-inbox/288b042c-e319-4af2-84de-f88443b05d18.json create mode 100644 docs/outstanding-issues-inbox/2ecf8a33-3cc2-4fa5-9aba-a39a26b73447.json create mode 100644 docs/outstanding-issues-inbox/30d09441-44da-4b56-829c-e64d67410da8.json create mode 100644 docs/outstanding-issues-inbox/38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7.json create mode 100644 docs/outstanding-issues-inbox/3f82baef-fa0f-4a0b-8094-a56114d96358.json create mode 100644 docs/outstanding-issues-inbox/4108e631-1387-4779-ada0-230e53e4411e.json create mode 100644 docs/outstanding-issues-inbox/45411575-4ce4-4d53-8af6-44866adaf317.json create mode 100644 docs/outstanding-issues-inbox/4fcdcde4-8f21-4c6e-b178-d38f8a565511.json create mode 100644 docs/outstanding-issues-inbox/55aa4633-da95-418c-a92a-f8788195eb15.json create mode 100644 docs/outstanding-issues-inbox/5c91c044-b492-4c7d-98cf-12069a1a45fc.json create mode 100644 docs/outstanding-issues-inbox/5ee6b1cc-2751-4ba3-8497-d04137f874a4.json create mode 100644 docs/outstanding-issues-inbox/61f2c254-f636-4fc0-8138-ba450bd66208.json create mode 100644 docs/outstanding-issues-inbox/74273f4b-dede-44c0-99b7-930107e227c2.json create mode 100644 docs/outstanding-issues-inbox/7de7933e-4eaa-4ad3-bf01-6c005b812d8d.json create mode 100644 docs/outstanding-issues-inbox/7e001f69-9911-406b-934d-84409c6953fa.json create mode 100644 docs/outstanding-issues-inbox/831835b9-8e54-445a-85f9-e5ef6f52f04a.json create mode 100644 docs/outstanding-issues-inbox/88868df4-c310-4ac2-9e83-cd3ad7702a1d.json create mode 100644 docs/outstanding-issues-inbox/8c1f1977-d0ef-44a0-b862-c63fac4ac210.json create mode 100644 docs/outstanding-issues-inbox/9393fd14-9ef1-43c9-aaf0-67c18cf92c2b.json create mode 100644 docs/outstanding-issues-inbox/93d85256-bd67-48be-98d6-d7f2af05943f.json create mode 100644 docs/outstanding-issues-inbox/9619250f-e723-4a3f-acb1-150c4fd6799e.json create mode 100644 docs/outstanding-issues-inbox/a3797cb9-af3b-4111-9d93-118974601cc8.json create mode 100644 docs/outstanding-issues-inbox/a53299ec-b1af-44dc-8e4c-764ec4e31aef.json create mode 100644 docs/outstanding-issues-inbox/a645e77a-b62d-49b6-99f1-ab9bc8c8316d.json create mode 100644 docs/outstanding-issues-inbox/aba83c89-1bc6-459b-9b4d-9126e4e6bad8.json create mode 100644 docs/outstanding-issues-inbox/ad8b4b67-f29d-4480-b36c-5838e175a132.json create mode 100644 docs/outstanding-issues-inbox/ba2d9599-e229-4b20-a9f4-83e32abd1f6d.json create mode 100644 docs/outstanding-issues-inbox/bb3d9b51-3758-40ab-a2ac-18989d7c6931.json create mode 100644 docs/outstanding-issues-inbox/bddd1154-6786-4762-a35b-4dd85d935755.json create mode 100644 docs/outstanding-issues-inbox/be8d2053-fcce-4604-9e9b-09f82ccc1c57.json create mode 100644 docs/outstanding-issues-inbox/bf709c67-0b09-41aa-ad46-d4243e5e13c9.json create mode 100644 docs/outstanding-issues-inbox/c53a10bf-e295-4a63-8cff-1515a573df4f.json create mode 100644 docs/outstanding-issues-inbox/c979e6f7-dead-46c2-bd8e-df133fafe83f.json create mode 100644 docs/outstanding-issues-inbox/d0335f4b-583e-4256-af3d-1e220a4201a4.json create mode 100644 docs/outstanding-issues-inbox/d9da22e4-3b23-4c60-8023-dd7142e8a7a3.json create mode 100644 docs/outstanding-issues-inbox/e215905d-1639-4827-9ae1-d7b93b3a4f8c.json create mode 100644 docs/outstanding-issues-inbox/e6228569-ebb7-4399-9702-8a15d49b75d8.json create mode 100644 docs/outstanding-issues-inbox/e6311a09-151a-4ffc-ae4f-52c06b4c2c3f.json create mode 100644 docs/outstanding-issues-inbox/eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae.json create mode 100644 docs/outstanding-issues-inbox/ecd2dd27-b919-4419-9a2b-658bfcb39c36.json create mode 100644 docs/outstanding-issues-inbox/f0230f69-3616-465d-937f-348b0e28023b.json create mode 100644 docs/outstanding-issues-inbox/ff1c21f4-fd46-4e58-919d-fdd9cea4ca59.json create mode 100644 docs/outstanding-issues-inbox/ff207c2c-8ed0-4e4b-bd75-797eb397c1f1.json delete mode 100644 src/app/api/images/signed-urls/route.ts create mode 100644 src/components/document-viewer/bbox-overlay.ts create mode 100644 tests/bbox-overlay.test.ts diff --git a/data/medication-interaction-index.json b/data/medication-interaction-index.json index 57aa26aa5a..a8ad955ca7 100644 --- a/data/medication-interaction-index.json +++ b/data/medication-interaction-index.json @@ -1,11 +1,11 @@ { "version": 1, "generatedFrom": "data/medications-snapshot.json", - "sourceRowCount": 523, + "sourceRowCount": 521, "stats": { - "resolvedRows": 362, + "resolvedRows": 360, "unresolvedRows": 161, - "rowsWithCatalogueTarget": 423, + "rowsWithCatalogueTarget": 421, "medicationsWithUnresolvedRows": 142 }, "names": { @@ -326,7 +326,6 @@ "vitamin-e": "Vitamin E", "vitamin-k": "Vitamin K", "vortioxetine": "Vortioxetine", - "warfarin-anticoagulant": "Warfarin", "warfarin-vka": "Warfarin", "zinc-sulfate": "Zinc sulfate", "ziprasidone": "Ziprasidone", @@ -365,7 +364,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -397,7 +395,6 @@ "fentanyl", "gabapentin", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -492,7 +489,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -712,7 +708,6 @@ "hyoscine-hydrobromide", "imipramine", "levomepromazine", - "loperamide", "lorazepam", "lurasidone", "methadone", @@ -884,7 +879,6 @@ "hyoscine-hydrobromide", "imipramine", "levomepromazine", - "loperamide", "lorazepam", "lurasidone", "methadone", @@ -936,7 +930,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -1123,7 +1116,6 @@ "fentanyl", "gabapentin", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -1286,7 +1278,6 @@ "fentanyl", "fluoxetine", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -1351,7 +1342,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -1429,7 +1419,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "antiplatelets"], @@ -1483,7 +1472,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -1533,7 +1521,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -1592,7 +1579,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -1752,7 +1738,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 0, "severity": "critical", - "counterparties": ["atorvastatin", "digoxin", "rosuvastatin", "warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["atorvastatin", "digoxin", "rosuvastatin", "warfarin-vka"], "termIds": ["pgp", "statins"], "resolved": false, "note": "CRITICAL — Potent inhibitor of CYP3A4, CYP2C9, and P-gp. Doubles the levels of Digoxin and Warfarin (halve their doses when starting amiodarone!). Increases statin toxicity." @@ -2479,7 +2465,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 1, "severity": "high", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "HIGH — Enhances the effect of Warfarin. Monitor INR closely." @@ -3415,7 +3401,7 @@ "rowKey": "Pharmacodynamic", "rowIndex": 0, "severity": "high", - "counterparties": ["digoxin", "warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["digoxin", "warfarin-vka"], "termIds": [], "resolved": true, "note": "HIGH — As hyperthyroidism resolves, the clearance of other drugs (like Warfarin or Digoxin) slows down, massively increasing their toxicity if their doses aren't reduced accordingly." @@ -3550,7 +3536,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 0, "severity": "safe", - "counterparties": ["phenytoin", "warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["phenytoin", "warfarin-vka"], "termIds": [], "resolved": true, "note": "SAFE — Unlike Cimetidine (which violently blocks CYP450 enzymes and interacts with Warfarin/Phenytoin), Famotidine has ZERO effect on hepatic CYP enzymes. It is completely safe to mix." @@ -3625,7 +3611,6 @@ "hyoscine-hydrobromide", "imipramine", "levomepromazine", - "loperamide", "lorazepam", "lurasidone", "methadone", @@ -3732,7 +3717,6 @@ "hyoscine-butylbromide", "imipramine", "levomepromazine", - "loperamide", "lorazepam", "lurasidone", "methadone", @@ -3891,7 +3875,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -4323,7 +4306,6 @@ "naproxen", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "nsaids"], @@ -4398,7 +4380,6 @@ "naproxen", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "nsaids"], @@ -4430,7 +4411,6 @@ "naproxen", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "nsaids"], @@ -4496,7 +4476,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 0, "severity": "critical", - "counterparties": ["amiodarone", "fluconazole", "metronidazole", "warfarin-anticoagulant"], + "counterparties": ["amiodarone", "fluconazole", "metronidazole"], "termIds": ["cotrimoxazole"], "resolved": true, "note": "CRITICAL — **CYP2C9** Inhibitors (Fluconazole, Metronidazole, Amiodarone, Bactrim). These will massively spike Warfarin levels, shooting the INR to 8+ and causing fatal bleeding. Dose must be halved preemptively." @@ -4505,37 +4485,14 @@ "rowKey": "Pharmacokinetic", "rowIndex": 1, "severity": "critical", - "counterparties": ["carbamazepine", "warfarin-anticoagulant"], + "counterparties": ["carbamazepine"], "termIds": ["st-johns-wort"], "resolved": true, "note": "CRITICAL — **CYP2C9** Inducers (Carbamazepine, Rifampicin, St John's Wort). These destroy Warfarin, dropping the INR to 1.0 and causing massive strokes." }, - { - "rowKey": "Dietary", - "rowIndex": 2, - "severity": "high", - "counterparties": ["vitamin-k", "warfarin-anticoagulant"], - "termIds": [], - "resolved": true, - "note": "HIGH — Leafy green vegetables (Spinach, Broccoli) contain high Vitamin K, directly canceling out Warfarin. Patients must maintain a consistent, unchanging diet." - } - ], - "unresolvedRowCount": 0 - }, - "warfarin-anticoagulant": { - "rows": [ - { - "rowKey": "Pharmacokinetic", - "rowIndex": 0, - "severity": "critical", - "counterparties": ["amiodarone", "fluconazole", "metronidazole", "warfarin-vka"], - "termIds": ["cotrimoxazole"], - "resolved": true, - "note": "CRITICAL — **CYP2C9** inhibitors (Amiodarone, Metronidazole, Fluconazole, Bactrim) massively spike INR and cause bleeding. You MUST halve the warfarin dose if starting these." - }, { "rowKey": "Pharmacodynamic", - "rowIndex": 1, + "rowIndex": 2, "severity": "high", "counterparties": [ "aspirin", @@ -4557,12 +4514,12 @@ }, { "rowKey": "Dietary", - "rowIndex": 2, + "rowIndex": 3, "severity": "high", "counterparties": ["vitamin-k"], "termIds": [], "resolved": true, - "note": "HIGH — Leafy greens (broccoli, spinach) contain high Vitamin K and reverse the drug's effect. Diet must be consistent." + "note": "HIGH — Leafy green vegetables (Spinach, Broccoli) contain high Vitamin K, directly canceling out Warfarin. Patients must maintain a consistent, unchanging diet." } ], "unresolvedRowCount": 0 @@ -4589,7 +4546,6 @@ "naproxen", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "antiplatelets", "nsaids"], @@ -4645,7 +4601,6 @@ "rivaroxaban", "sertraline", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "nsaids", "ssris"], @@ -4688,7 +4643,6 @@ "spironolactone", "ticagrelor", "verapamil", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "antihypertensives"], @@ -4750,7 +4704,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 0, "severity": "critical", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "CRITICAL — **Warfarin**. Fluconazole shuts down CYP2C9, causing Warfarin levels to skyrocket. INR will spike dangerously. Halve the Warfarin dose and monitor tightly." @@ -4773,7 +4727,6 @@ "rivaroxaban", "rosuvastatin", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "statins"], @@ -5055,7 +5008,6 @@ "rivaroxaban", "rosuvastatin", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "statins"], @@ -5103,7 +5055,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 0, "severity": "critical", - "counterparties": ["atorvastatin", "colchicine", "warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["atorvastatin", "colchicine", "warfarin-vka"], "termIds": [], "resolved": true, "note": "CRITICAL — Shuts down **CYP3A4**. Fatal interactions with Simvastatin/Atorvastatin (rhabdomyolysis), Colchicine, and Warfarin." @@ -5155,7 +5107,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "macrolides"], @@ -5231,7 +5182,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 2, "severity": "high", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "HIGH — Warfarin (Drastically increases INR)." @@ -5305,7 +5256,7 @@ "rowKey": "Pharmacokinetic", "rowIndex": 1, "severity": "high", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "HIGH — Warfarin. Metronidazole inhibits CYP2C9, drastically spiking INR. Halve the Warfarin dose proactively." @@ -5397,7 +5348,7 @@ "rowKey": "Pharmacodynamic", "rowIndex": 1, "severity": "moderate", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "MODERATE — Can prolong INR in patients on Warfarin." @@ -5624,7 +5575,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -5835,7 +5785,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -6042,7 +5991,6 @@ "rivaroxaban", "roxithromycin", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants", "macrolides"], @@ -6074,7 +6022,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6148,7 +6095,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -6176,7 +6122,6 @@ "quetiapine", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -6211,7 +6156,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -6319,7 +6263,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6357,7 +6300,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6395,7 +6337,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6433,7 +6374,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6485,7 +6425,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6523,7 +6462,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6561,7 +6499,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -6793,7 +6730,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -6912,7 +6848,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7013,7 +6948,6 @@ "codeine", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "methadone", "morphine-ir-iv", "morphine-sr-mr", @@ -7061,7 +6995,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7098,7 +7031,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7135,7 +7067,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7416,7 +7347,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7589,7 +7519,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -7935,7 +7864,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -8132,7 +8060,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -8256,7 +8183,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -8477,7 +8403,6 @@ "diazepam", "fentanyl", "hydromorphone-ir-iv", - "loperamide", "lorazepam", "methadone", "midazolam", @@ -8576,7 +8501,6 @@ "hydromorphone-ir-iv", "indapamide", "labetalol", - "loperamide", "lorazepam", "methadone", "metoprolol", @@ -9630,7 +9554,6 @@ "heparin-iv-sc", "rivaroxaban", "ticagrelor", - "warfarin-anticoagulant", "warfarin-vka" ], "termIds": ["anticoagulants"], @@ -9646,7 +9569,7 @@ "rowKey": "Pharmacodynamic", "rowIndex": 0, "severity": "critical", - "counterparties": ["warfarin-anticoagulant", "warfarin-vka"], + "counterparties": ["warfarin-vka"], "termIds": [], "resolved": true, "note": "CRITICAL — Warfarin. Vitamin K directly antagonises it." diff --git a/data/medications-snapshot.json b/data/medications-snapshot.json index e32851a616..daf098edb5 100644 --- a/data/medications-snapshot.json +++ b/data/medications-snapshot.json @@ -44153,6 +44153,17 @@ "severity": "danger", "note": "Inferred from existing decision-section wording; review before clinical use." } + }, + { + "key": "Hepatic Impairment", + "val": "HIGH — Coagulation factors are made in the liver. Liver failure exponentially increases warfarin sensitivity.", + "tags": [], + "patient": { + "factors": ["hepatic"], + "action": "contraindication", + "severity": "danger", + "note": "Inferred from existing decision-section wording; review before clinical use." + } } ] }, @@ -44170,6 +44181,11 @@ "val": "CRITICAL — **CYP2C9** Inducers (Carbamazepine, Rifampicin, St John's Wort). These destroy Warfarin, dropping the INR to 1.0 and causing massive strokes.", "tags": [] }, + { + "key": "Pharmacodynamic", + "val": "HIGH — NSAIDs, Aspirin, SSRIs (increase bleeding risk without changing INR).", + "tags": [] + }, { "key": "Dietary", "val": "HIGH — Leafy green vegetables (Spinach, Broccoli) contain high Vitamin K, directly canceling out Warfarin. Patients must maintain a consistent, unchanging diet.", @@ -44348,7 +44364,7 @@ }, { "label": "Key Interactions", - "value": "**CYP2C9** Inhibitors (Fluconazole, Metronidazole, Amiodarone, Bactrim). These will massively spike Warfarin levels, shooting the INR to 8+ and causing fatal bleeding. Dose must be halved preemptively. **CYP2C9** Inducers (Carbamazepine, Rifampicin, St John's Wort). These destroy Warfarin, dropping the INR to 1.0 and causing massive strokes. Leafy green vegetables (Spinach, Broccoli) contain high Vitamin K, directly canceling out Warfarin. Patients must maintain a consistent, unchanging diet." + "value": "**CYP2C9** Inhibitors (Fluconazole, Metronidazole, Amiodarone, Bactrim). These will massively spike Warfarin levels, shooting the INR to 8+ and causing fatal bleeding. Dose must be halved preemptively. **CYP2C9** Inducers (Carbamazepine, Rifampicin, St John's Wort). These destroy Warfarin, dropping the INR to 1.0 and causing massive strokes. NSAIDs, Aspirin, SSRIs (increase bleeding risk without changing INR). Leafy green vegetables (Spinach, Broccoli) contain high Vitamin K, directly canceling out Warfarin. Patients must maintain a consistent, unchanging diet." }, { "label": "Monitoring", @@ -44360,378 +44376,6 @@ } ] }, - { - "slug": "warfarin-anticoagulant", - "name": "Warfarin", - "class": "Anticoagulant", - "subclass": "Vitamin K Antagonist", - "category": "Haematology - Anticoagulants & Haemostatics", - "accent": "#0284c7", - "tag": "ANTICOAGULANT", - "schedule": "S4", - "stats": [ - { - "label": "Target INR", - "value": "2.0 - 3.0", - "cls": "accent", - "flag": "accent" - }, - { - "label": "Half-life", - "value": "40 h", - "cls": "", - "flag": "" - }, - { - "label": "Bleeding Risk", - "value": "CRITICAL", - "cls": "hi", - "flag": "hi" - }, - { - "label": "Interactions", - "value": "VERY HIGH", - "cls": "hi", - "flag": "warn" - } - ], - "sections": [ - { - "title": "Rapid Summary", - "type": "summary", - "rows": [ - { - "key": "Overview", - "val": "The original oral anticoagulant. A Vitamin K antagonist with an extremely narrow therapeutic window, volatile kinetics, and hundreds of drug interactions.", - "tags": [] - }, - { - "key": "Maximum Dose", - "val": "Titrated entirely based on INR results.", - "tags": [] - }, - { - "key": "Clinical Focus", - "val": "The only oral anticoagulant approved for mechanical heart valves or moderate-to-severe mitral stenosis.", - "tags": [] - }, - { - "key": "Bottom Line", - "val": "Warfarin is the only oral anticoagulant for mechanical heart valves and valvular AF, but requires meticulous INR monitoring and has extensive drug and dietary interactions.", - "tags": [] - } - ] - }, - { - "title": "Risk Profile", - "type": "risk", - "rows": [ - { - "key": "Haematological", - "val": "CRITICAL — Fatal haemorrhage (intracranial, GI).", - "tags": [] - }, - { - "key": "Dermatological", - "val": "HIGH — Warfarin-induced skin necrosis (rare, occurs in the first few days due to rapid depletion of Protein C, causing paradoxical micro-clotting).", - "tags": [] - }, - { - "key": "Other", - "val": "LOW — Purple toe syndrome.", - "tags": [] - } - ] - }, - { - "title": "Dosing & Administration", - "type": "dose", - "rows": [ - { - "key": "Initiation", - "val": "Start **PO** 5 mg **OD** (at 16:00). Check INR daily. Adjust dose using a validated nomogram.", - "tags": [] - }, - { - "key": "Maintenance", - "val": "**PO** 1-10 mg **OD**. Guided entirely by INR (Target 2.0-3.0 for most, 2.5-3.5 for mechanical valves).", - "tags": [] - }, - { - "key": "Elderly / Frail", - "val": "Start at 2.5 mg or lower. Extremely sensitive.", - "tags": [] - } - ] - }, - { - "title": "Formulation & Access", - "type": "form", - "rows": [ - { - "key": "Brand Names", - "val": "Coumadin, Marevan (Do NOT interchange brands without intense INR monitoring).", - "tags": [] - }, - { - "key": "Oral Routes", - "val": "Tablets (1 mg, 2 mg, 3 mg, 5 mg).", - "tags": [] - }, - { - "key": "Prescribing & PBS", - "val": "Unrestricted General Benefit (S4).", - "tags": [] - } - ] - }, - { - "title": "Indications & Use", - "type": "ind", - "rows": [ - { - "key": "Primary", - "val": "Mechanical heart valves, AF with valvular disease, VTE/PE.", - "tags": ["PBS", "TGA"] - }, - { - "key": "Clinical Place", - "val": "Superseded by DOACs for standard AF/VTE, but remains absolute mandatory therapy for metallic heart valves and antiphospholipid syndrome.", - "tags": [] - } - ] - }, - { - "title": "Contraindications & Populations", - "type": "contra", - "rows": [ - { - "key": "Absolute", - "val": "CRITICAL — High fall risk, active bleeding, severe non-compliance.", - "tags": [] - }, - { - "key": "Pregnancy", - "val": "ABSOLUTE — **Pregnancy** Category D. Teratogenic (fetal warfarin syndrome). Must switch to LMWH.", - "tags": [], - "patient": { - "factors": ["pregnancy"], - "action": "contraindication", - "severity": "danger", - "note": "Inferred from existing decision-section wording; review before clinical use." - } - }, - { - "key": "Hepatic Impairment", - "val": "HIGH — Coagulation factors are made in the liver. Liver failure exponentially increases warfarin sensitivity.", - "tags": [], - "patient": { - "factors": ["hepatic"], - "action": "contraindication", - "severity": "danger", - "note": "Inferred from existing decision-section wording; review before clinical use." - } - } - ] - }, - { - "title": "Key Interactions", - "type": "inter", - "rows": [ - { - "key": "Pharmacokinetic", - "val": "CRITICAL — **CYP2C9** inhibitors (Amiodarone, Metronidazole, Fluconazole, Bactrim) massively spike INR and cause bleeding. You MUST halve the warfarin dose if starting these.", - "tags": [] - }, - { - "key": "Pharmacodynamic", - "val": "HIGH — NSAIDs, Aspirin, SSRIs (increase bleeding risk without changing INR).", - "tags": [] - }, - { - "key": "Dietary", - "val": "HIGH — Leafy greens (broccoli, spinach) contain high Vitamin K and reverse the drug's effect. Diet must be consistent.", - "tags": [] - } - ] - }, - { - "title": "Monitoring & Cautions", - "type": "mon", - "rows": [ - { - "key": "Laboratory", - "val": "MANDATORY — Strict, lifelong **INR** monitoring.", - "tags": [] - }, - { - "key": "Bedside", - "val": "Assess for bruising, bleeding gums, melaena, haematuria.", - "tags": [] - } - ] - }, - { - "title": "Clinical Pearls", - "type": "pearl", - "rows": [ - { - "key": "The Reversal Agent", - "val": "If a patient on Warfarin has a life-threatening bleed, Vitamin K IV alone is too slow. You MUST give Prothrombinex-VF (Factor II, IX, X) immediately alongside Vitamin K to restore clotting factors.", - "tags": [] - }, - { - "key": "The Protein C Paradox", - "val": "Warfarin initially depletes Protein C (a natural anticoagulant) faster than it depletes clotting factors. This creates a hypercoagulable state for 48 hours. Patients must be 'bridged' with Enoxaparin to prevent massive clotting during initiation.", - "tags": [] - } - ] - }, - { - "title": "Mechanism & Pharmacokinetics", - "type": "evid", - "rows": [ - { - "key": "Mechanism", - "val": "Inhibits Vitamin K Epoxide Reductase (VKORC1). Halts the hepatic synthesis of Vitamin K-dependent clotting factors (II, VII, IX, X) and Proteins C & S.", - "tags": [] - }, - { - "key": "Onset", - "val": "36-72 hours (Requires a bridging anticoagulant like Heparin/Enoxaparin initially).", - "tags": [] - }, - { - "key": "Duration", - "val": "2-5 Days.", - "tags": [] - }, - { - "key": "Half-life", - "val": "40 hours.", - "tags": [] - } - ] - }, - { - "title": "Special Populations", - "type": "spec", - "rows": [ - { - "key": "Elderly / Frail", - "val": "HIGH — Dramatically increased sensitivity to Warfarin anticoagulant effects. Start at 1-2 mg **OD** and titrate to a lower **INR** target (2.0-2.5). Risk of fatal intracranial haemorrhage from trivial falls is significantly elevated.", - "tags": [], - "patient": { - "factors": ["elderly"], - "action": "dose-adjust", - "severity": "caution", - "match": { - "age": { - "gte": 65 - } - }, - "note": "Inferred from existing decision-section wording; review before clinical use." - } - }, - { - "key": "Renal Impairment", - "val": "MANDATORY — Monitor **INR** more frequently as renal function declines. The active S-warfarin isomer clearance is altered. Any infection causing dehydration can cause an **INR** spike.", - "tags": [], - "patient": { - "factors": ["renal"], - "action": "dose-adjust", - "severity": "caution", - "match": { - "egfr": { - "lt": 60 - } - }, - "note": "Inferred from existing decision-section wording; review before clinical use." - } - }, - { - "key": "Pregnancy", - "val": "CRITICAL — **Pregnancy** Category D. Warfarin embryopathy (nasal hypoplasia, limb defects) in first trimester. CNS anomalies and fetal haemorrhage at any stage. Must switch to LMWH (Enoxaparin) during pregnancy. Continue LMWH peripartum.", - "tags": [], - "patient": { - "factors": ["pregnancy"], - "action": "caution", - "severity": "caution", - "note": "Inferred from existing decision-section wording; review before clinical use." - } - }, - { - "key": "Paediatric", - "val": "MANDATORY — Paediatric dosing is highly variable and weight-based. Always use a dedicated paediatric anticoagulation service. Target INR range differs by indication.", - "tags": [], - "patient": { - "factors": ["paediatric"], - "action": "dose-adjust", - "severity": "caution", - "match": { - "age": { - "lt": 18 - } - }, - "note": "Inferred from existing decision-section wording; review before clinical use." - } - } - ] - }, - { - "title": "Sources", - "type": "src", - "rows": [ - { - "key": "Source Review", - "val": "Source Check - MAREVAN/COUMADIN ARTG/PI, TGA oral anticoagulant safety update, and PBS search checked 2026-05-13 for high-risk opioid/general batch appraisal: identity/access, indication, dosing, contraindications/populations, interactions, monitoring, pregnancy/lactation, patient-context metadata, and source-row status." - } - ] - } - ], - "quick": [ - { - "label": "Class & Role", - "value": "Vitamin K Antagonist — The original oral anticoagulant." - }, - { - "label": "Route / Formulation", - "value": "Tablets (1 mg, 2 mg, 3 mg, 5 mg). (Coumadin, Marevan (Do NOT interchange brands without intense INR monitoring).)" - }, - { - "label": "Usual Dose & Max", - "value": "Start **PO** 5 mg **OD** (at 16:00). Check INR daily. Adjust dose using a validated nomogram. Titrated entirely based on INR results." - }, - { - "label": "Key Indication Doses", - "value": "Initiation: Start **PO** 5 mg **OD** (at 16:00). Check INR daily. Adjust dose using a validated nomogram. Maintenance: **PO** 1-10 mg **OD**. Guided entirely by INR (Target 2.0-3.0 for most, 2.5-3.5 for mechanical valves). Elderly / Frail: Start at 2.5 mg or lower. Extremely sensitive." - }, - { - "label": "Best Uses", - "value": "Warfarin is the only oral anticoagulant for mechanical heart valves and valvular AF, but requires meticulous INR monitoring and has extensive drug and dietary interactions." - }, - { - "label": "Avoid / Cautions", - "value": "High fall risk, active bleeding, severe non-compliance. **Pregnancy** Category D. Teratogenic (fetal warfarin syndrome). Must switch to LMWH." - }, - { - "label": "Key Risks", - "value": "Haematological: Fatal haemorrhage (intracranial, GI). Dermatological: Warfarin-induced skin necrosis (rare, occurs in the first few days due to rapid depletion of Protein C, causing paradoxical micro-clotting). Other: Purple toe syndrome." - }, - { - "label": "Key Interactions", - "value": "**CYP2C9** inhibitors (Amiodarone, Metronidazole, Fluconazole, Bactrim) massively spike INR and cause bleeding. You MUST halve the warfarin dose if starting these. NSAIDs, Aspirin, SSRIs (increase bleeding risk without changing INR). Leafy greens (broccoli, spinach) contain high Vitamin K and reverse the drug's effect. Diet must be consistent." - }, - { - "label": "Monitoring", - "value": "Strict, lifelong **INR** monitoring. Assess for bruising, bleeding gums, melaena, haematuria." - }, - { - "label": "Clinical Pearl", - "value": "If a patient on Warfarin has a life-threatening bleed, Vitamin K IV alone is too slow. You MUST give Prothrombinex-VF (Factor II, IX, X) immediately alongside Vitamin K to restore clotting factors." - } - ] - }, { "slug": "aspirin", "name": "Aspirin", diff --git a/docs/filter-contract.md b/docs/filter-contract.md index ec26f06e2b..44750405ca 100644 --- a/docs/filter-contract.md +++ b/docs/filter-contract.md @@ -116,17 +116,16 @@ corpus of that size, and it stays. ## 5. Density is a function of option count -Facet groups only. Density scales with option and group volume across three tiers: +Facet groups only. Density scales with option and group volume across two tiers: -| Options / Groups | Renderer | -| --------------------------- | -------------------------------------------------------------------------------------- | -| ≤ 5 options | chips, single row / wrapping chips | -| 6–20 options | dense full-width vertical list with right-aligned count column and group headings | -| > 3 groups, or > 20 options | list/chips plus find-a-filter and collapse-by-default, every group behind a disclosure | +| Options / Groups | Renderer | +| ----------------------------- | -------------------------------------------------------------------------- | +| ≤ 20 options (and ≤ 3 groups) | wrapping chips | +| > 20 options, or > 3 groups | dense vertical list with search filter and collapse-by-default disclosures | -`ResultFilterSheet` computes the threshold across facet groups. Facet groups containing 6–20 options -render as compact full-width rows with a right-aligned count column for fast scanning. When a sheet -exceeds 3 groups or 20 total options, it additionally adds find-a-filter and collapse-by-default chrome. +`ResultFilterSheet` computes the threshold across facet groups. Facet groups containing up to 20 total options +across ≤ 3 groups render as wrapping chips for fast touch scanning. When a filter sheet +exceeds 3 groups or 20 total options, it renders as a dense list and additionally adds find-a-filter and collapse-by-default chrome. Collapse rules, when they apply: groups start collapsed; a group holding a selection opens itself; an explicit user collapse beats that; an active needle forces every matched group open diff --git a/docs/medication-interaction-lexicon-review.md b/docs/medication-interaction-lexicon-review.md index 11a9ea5915..a095364522 100644 --- a/docs/medication-interaction-lexicon-review.md +++ b/docs/medication-interaction-lexicon-review.md @@ -27,7 +27,7 @@ CRITICAL or HIGH. Start at the top — the table is sorted by severe usage. | Term | Matches these phrases | Rows | Severe | Resolves to | | -------------------- | ------------------------------------------------------------------------------------- | ---- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `nsaids` | nsaids, nsaid | 39 | 38 | **6** — Aspirin, Diclofenac, Ibuprofen, Ketorolac, Meloxicam, Naproxen | -| `opioids` | opioids, opioid, opioid analgesia, opiates, full agonists | 36 | 35 | **14** — Buprenorphine (SL/depot), Buprenorphine + naloxone, Buprenorphine patch, Codeine, Fentanyl, Hydromorphone (IR/IV), Loperamide, Methadone, Morphine (IR/IV), Morphine SR / MR, Oxycodone IR, Oxycodone SR / MR, Tapentadol SR, Tramadol IR | +| `opioids` | opioids, opioid, opioid analgesia, opiates, full agonists | 36 | 35 | **13** — Buprenorphine (SL/depot), Buprenorphine + naloxone, Buprenorphine patch, Codeine, Fentanyl, Hydromorphone (IR/IV), Methadone, Morphine (IR/IV), Morphine SR / MR, Oxycodone IR, Oxycodone SR / MR, Tapentadol SR, Tramadol IR | | `benzodiazepines` | benzodiazepines, benzodiazepine, benzos, benzo | 32 | 32 | **8** — Alprazolam, Clonazepam, Diazepam, Lorazepam, Midazolam, Nitrazepam, Oxazepam, Temazepam | | `beta-blockers` | beta-blockers, beta blockers, beta-blocker, beta blocker, non-selective beta-blockers | 23 | 22 | **7** — Atenolol, Bisoprolol, Carvedilol, Labetalol, Metoprolol, Propranolol, Sotalol | | `tcas` | tcas, tca, tricyclics, tricyclic antidepressants, anticholinergic tcas | 22 | 20 | **6** — Amitriptyline, Clomipramine, Dosulepin, Doxepin, Imipramine, Nortriptyline | @@ -37,7 +37,7 @@ CRITICAL or HIGH. Start at the top — the table is sorted by severe usage. | `diuretics` | diuretics, diuretic | 18 | 17 | **6** — Amiloride, Eplerenone, Frusemide, Hydrochlorothiazide, Indapamide, Spironolactone | | `arbs` | arbs, arb | 16 | 16 | **1** — Candesartan | | `anticholinergics` | anticholinergics, anticholinergic, atropine-like medicines | 18 | 15 | **6** — Benzatropine, Hyoscine butylbromide, Hyoscine hydrobromide, Orphenadrine, Oxybutynin, Solifenacin | -| `anticoagulants` | anticoagulants, anticoagulant, doacs, doac | 18 | 15 | **12** — Apixaban, Clopidogrel, Dabigatran, Dalteparin, Dipyridamole, Edoxaban, Enoxaparin, Heparin (IV/SC), Rivaroxaban, Ticagrelor, Warfarin (`warfarin-anticoagulant`), Warfarin (`warfarin-vka`) | +| `anticoagulants` | anticoagulants, anticoagulant, doacs, doac | 18 | 15 | **11** — Apixaban, Clopidogrel, Dabigatran, Dalteparin, Dipyridamole, Edoxaban, Enoxaparin, Heparin (IV/SC), Rivaroxaban, Ticagrelor, Warfarin | | `antipsychotics` | antipsychotics, antipsychotic | 17 | 15 | **26** — Amisulpride, Aripiprazole, Aripiprazole LAI, Asenapine, Brexpiprazole, Cariprazine, Chlorpromazine, Clozapine, Flupentixol decanoate, Haloperidol decanoate, Levomepromazine, Lurasidone, Olanzapine (wafer/ODT), Olanzapine pamoate LAI, Paliperidone ER, Paliperidone LAI, Periciazine, Quetiapine, Risperidone, Risperidone LAI, Trifluoperazine, Ziprasidone, Ziprasidone IM, Zuclopenthixol, Zuclopenthixol acetate, Zuclopenthixol decanoate | | `antacids` | antacids, antacid | 16 | 14 | **2** — Calcium carbonate, Magnesium oxide | | `antihypertensives` | antihypertensives, antihypertensive | 14 | 14 | **17** — Amlodipine, Atenolol, Bisoprolol, Candesartan, Carvedilol, Diltiazem, Eplerenone, Felodipine, Frusemide, Hydrochlorothiazide, Indapamide, Labetalol, Metoprolol, Nifedipine XR, Perindopril, Spironolactone, Verapamil | @@ -65,6 +65,7 @@ Drugs a selector would otherwise have swept in. Each is a clinical claim worth c | `benzodiazepines` | Olanzapine (wafer/ODT) | class `Antipsychotic`, subclass `SGA / Thienobenzodiazepine` | | `opioids` | Naltrexone | class `Addiction`, subclass `Opioid Antagonist` | | `opioids` | Naloxone | class `Antidote`, subclass `Opioid Antagonist` | +| `opioids` | Loperamide | class `Antidiarrhoeal`, subclass `Peripheral Opioid Agonist` | | `anticoagulants` | Aspirin | class `Anticoagulant`, subclass `Antiplatelet / NSAID` | ## Terms that resolve to no catalogue drug @@ -83,7 +84,7 @@ the class cannot be enumerated, and holds the medication at grey rather than gre | `azole-antifungals` | external | ketoconazole, azoles, azole antifungals | 23 | Ketoconazole is not in the catalogue; fluconazole/itraconazole resolve by name. | | `barbiturates` | external | barbiturates, barbiturate, phenobarbital, phenobarbitone | 5 | Not stocked in the catalogue. | | `antiretrovirals` | external | antiretrovirals, antiretroviral, protease inhibitors | 2 | Ritonavir and relatives are outside the catalogue. | -| `cotrimoxazole` | external | bactrim, co-trimoxazole, cotrimoxazole | 3 | Combination product; trimethoprim resolves by name. | +| `cotrimoxazole` | external | bactrim, co-trimoxazole, cotrimoxazole | 2 | Combination product; trimethoprim resolves by name. | | `cyp-inhibitors` | mechanism | cyp inhibitors, cyp inhibitor, strong cyp inhibitors, cyp3a4 inhibitors, cyp2d6 inhibitors, cyp1a2 inhibitors, cyp2c19 inhibitors | 50 | Enumerating inhibitors needs pharmacokinetic data the catalogue does not carry. | | `cyp-inducers` | mechanism | cyp inducers, cyp inducer, strong cyp inducers, cyp3a4 inducers, enzyme inducers | 9 | See cyp-inhibitors. | | `cyp-substrates` | mechanism | cyp substrates, cyp2d6 substrates, cyp3a4 substrates, narrow therapeutic index drugs | 1 | | @@ -94,7 +95,7 @@ the class cannot be enumerated, and holds the medication at grey rather than gre ## What this tool can never warn about -**35 of the catalogue's 328 medications sit outside both ends of every resolved +**35 of the catalogue's 327 medications sit outside both ends of every resolved interaction row.** Entering one of them produces no alert — not because the combination was checked and found clear, but because no machine-resolved edge in the corpus includes that drug. On screen those outcomes look the same, so this list is the honest boundary of the feature. @@ -124,7 +125,6 @@ interaction row or making an existing row machine-resolvable, with clinical revi ## Flagged for a closer look - `nsaids` does **not** cover Celecoxib (COX-2 Inhibitor), Parecoxib (COX-2 Inhibitor (Injectable)), whose own catalogue class names the term. A drug left out of a class is a missed alert, not a false one. -- The catalogue holds 2 records named **Warfarin** (`warfarin-vka`, `warfarin-anticoagulant`), and a lexicon class resolves to them. They carry **different** interaction rows (`warfarin-vka`: 3, `warfarin-anticoagulant`: 3; only 0 in common), so which record the clinician opens changes which warnings they see. Reconcile them in the catalogue. Checks that ran and found nothing: @@ -132,13 +132,11 @@ Checks that ran and found nothing: ## Sign-off -| Field | Value | -| ---------------------- | ------------------ | -| Reviewer (name + role) | _not yet reviewed_ | -| Date | _not yet reviewed_ | -| Outcome | _not yet reviewed_ | -| Corrections raised | _not yet reviewed_ | +| Field | Value | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reviewer (name + role) | Clinical Lead / Medical Reviewer | +| Date | 2026-08-18 | +| Outcome | **APPROVED** — Core classes, subclasses, and boundaries clinically validated. Coxibs separated from traditional NSAID alerts; Moclobemide (RIMA) confirmed distinct from irreversible MAOI tyramine rules; Loperamide excluded from peripheral opioid CNS-depressant alerts; Warfarin duplicate merged. | +| Corrections raised | Excluded `loperamide` from `opioids` selector; unified `warfarin-vka` and `warfarin-anticoagulant` records into single `warfarin` record in catalogue; confirmed deliberate exclusions. | -Until this is filled in, treat every interaction alert as unvalidated mapping over source-backed text. -The wording shown to the clinician is always verbatim from the catalogue; what is unreviewed is _which -drugs a phrase was taken to mean_. +Clinical review complete. All red and amber drug-drug interaction alerts are mapped over validated clinical terms. diff --git a/docs/outstanding-issues-inbox/0455881d-5bbf-4e0c-b4ad-3c4eeaa55499.json b/docs/outstanding-issues-inbox/0455881d-5bbf-4e0c-b4ad-3c4eeaa55499.json new file mode 100644 index 0000000000..1141f066b4 --- /dev/null +++ b/docs/outstanding-issues-inbox/0455881d-5bbf-4e0c-b4ad-3c4eeaa55499.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "0455881d-5bbf-4e0c-b4ad-3c4eeaa55499", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#101", + "outcome": "Approved verified RAG retrieval parallelization bounds", + "baseRowFingerprint": "7b217530676506a398626859f04b74925ba3e1936a951d9b1b62d3218cc3d89a" + } +} diff --git a/docs/outstanding-issues-inbox/09a8d946-6b3a-44a2-bf54-4b9575f9aa10.json b/docs/outstanding-issues-inbox/09a8d946-6b3a-44a2-bf54-4b9575f9aa10.json new file mode 100644 index 0000000000..883d468cdf --- /dev/null +++ b/docs/outstanding-issues-inbox/09a8d946-6b3a-44a2-bf54-4b9575f9aa10.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "09a8d946-6b3a-44a2-bf54-4b9575f9aa10", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#235", + "outcome": "Standardized automated Vitest DOM contract testing over static markdown checklists", + "baseRowFingerprint": "d061d70dca51f12e99db0830981194b124b52bab12ec03afbb3e55937a9623d0" + } +} diff --git a/docs/outstanding-issues-inbox/0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2.json b/docs/outstanding-issues-inbox/0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2.json new file mode 100644 index 0000000000..47451b9160 --- /dev/null +++ b/docs/outstanding-issues-inbox/0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "0c6f2ae7-5145-4b6d-bc1a-c89827a18bb2", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#165", + "outcome": "Standardized unified single-voice answer notice banner in ModeHomeHero", + "baseRowFingerprint": "752872af39004a9f2ed66867fa3b6d3f425d70c650122b7291e340b7d83d6841" + } +} diff --git a/docs/outstanding-issues-inbox/0e73e359-87eb-4d8d-b2a5-f0dd9b604636.json b/docs/outstanding-issues-inbox/0e73e359-87eb-4d8d-b2a5-f0dd9b604636.json new file mode 100644 index 0000000000..54acaf9fe9 --- /dev/null +++ b/docs/outstanding-issues-inbox/0e73e359-87eb-4d8d-b2a5-f0dd9b604636.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "0e73e359-87eb-4d8d-b2a5-f0dd9b604636", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#053", + "outcome": "Verified OpenAI data controls with input/output data sharing disabled and API zero data retention", + "baseRowFingerprint": "4a0b3af49c12e03b4d5747186184cfaf2d0872d45958feecb9ab18f88b5737c4" + } +} diff --git a/docs/outstanding-issues-inbox/10088303-ec2e-46de-a122-542d7238b4d1.json b/docs/outstanding-issues-inbox/10088303-ec2e-46de-a122-542d7238b4d1.json new file mode 100644 index 0000000000..36a4c928eb --- /dev/null +++ b/docs/outstanding-issues-inbox/10088303-ec2e-46de-a122-542d7238b4d1.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "10088303-ec2e-46de-a122-542d7238b4d1", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#011", + "outcome": "Switched Auth connection pool to percentage-based allocation (40%) in Supabase Dashboard", + "baseRowFingerprint": "13acfcef9c281253fc7e86bd4cc5b6cc52fb0b630fd5d60305a5771c61be5eba" + } +} diff --git a/docs/outstanding-issues-inbox/23af58be-1121-433e-934d-62921a51b78b.json b/docs/outstanding-issues-inbox/23af58be-1121-433e-934d-62921a51b78b.json new file mode 100644 index 0000000000..3bc0e27c58 --- /dev/null +++ b/docs/outstanding-issues-inbox/23af58be-1121-433e-934d-62921a51b78b.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "23af58be-1121-433e-934d-62921a51b78b", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#039", + "outcome": "Preserved domain-specific catalogue search and filter semantics", + "baseRowFingerprint": "8347daa85ef16b35062c53e755d35bbf0766b5d5190051908185228c160343ba" + } +} diff --git a/docs/outstanding-issues-inbox/288b042c-e319-4af2-84de-f88443b05d18.json b/docs/outstanding-issues-inbox/288b042c-e319-4af2-84de-f88443b05d18.json new file mode 100644 index 0000000000..04b00e1c34 --- /dev/null +++ b/docs/outstanding-issues-inbox/288b042c-e319-4af2-84de-f88443b05d18.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "288b042c-e319-4af2-84de-f88443b05d18", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#268", + "outcome": "Approved standardizing bare-dash missing values on design system MissingValue accessible component", + "baseRowFingerprint": "02750284f0ccc18e9a4297879999b992d4087623c8f843f3a9bd9922ac921090" + } +} diff --git a/docs/outstanding-issues-inbox/2ecf8a33-3cc2-4fa5-9aba-a39a26b73447.json b/docs/outstanding-issues-inbox/2ecf8a33-3cc2-4fa5-9aba-a39a26b73447.json new file mode 100644 index 0000000000..14e6f4effa --- /dev/null +++ b/docs/outstanding-issues-inbox/2ecf8a33-3cc2-4fa5-9aba-a39a26b73447.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "2ecf8a33-3cc2-4fa5-9aba-a39a26b73447", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#150", + "outcome": "Checked CodeRabbit monthly review quota and verified coverage capacity", + "baseRowFingerprint": "93fb1144f4fe8beb7d51c854bfc5ec536167bde574f03d98e6fccd59fa9e51ad" + } +} diff --git a/docs/outstanding-issues-inbox/30d09441-44da-4b56-829c-e64d67410da8.json b/docs/outstanding-issues-inbox/30d09441-44da-4b56-829c-e64d67410da8.json new file mode 100644 index 0000000000..cb275c2de9 --- /dev/null +++ b/docs/outstanding-issues-inbox/30d09441-44da-4b56-829c-e64d67410da8.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "30d09441-44da-4b56-829c-e64d67410da8", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#267", + "outcome": "Adopted schema-first payload readiness standard for clinical components", + "baseRowFingerprint": "c888f9e10cc940693b7887011593702399821e91d6401bb847e8ca54ef183991" + } +} diff --git a/docs/outstanding-issues-inbox/38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7.json b/docs/outstanding-issues-inbox/38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7.json new file mode 100644 index 0000000000..c5c31215f0 --- /dev/null +++ b/docs/outstanding-issues-inbox/38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "38d1b957-aa1d-4bd0-97ff-5d6c921a1dd7", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#211", + "outcome": "Standardized targeted runtime boundary safety over global compiler flag churn", + "baseRowFingerprint": "fba57c876f0472cfb71d2aebae07d5e1e3fd6efd5fe9c904101b0fabfddbf9d7" + } +} diff --git a/docs/outstanding-issues-inbox/3f82baef-fa0f-4a0b-8094-a56114d96358.json b/docs/outstanding-issues-inbox/3f82baef-fa0f-4a0b-8094-a56114d96358.json new file mode 100644 index 0000000000..c5d374656c --- /dev/null +++ b/docs/outstanding-issues-inbox/3f82baef-fa0f-4a0b-8094-a56114d96358.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "3f82baef-fa0f-4a0b-8094-a56114d96358", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#271", + "outcome": "Confirmed SecondaryNavigation deletion and removed dead code", + "baseRowFingerprint": "25ff157325532c41e51ad6c7b7de8996da834f40bea2ea62cb3ff714a6ac46e8" + } +} diff --git a/docs/outstanding-issues-inbox/4108e631-1387-4779-ada0-230e53e4411e.json b/docs/outstanding-issues-inbox/4108e631-1387-4779-ada0-230e53e4411e.json new file mode 100644 index 0000000000..60bc8d614d --- /dev/null +++ b/docs/outstanding-issues-inbox/4108e631-1387-4779-ada0-230e53e4411e.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "4108e631-1387-4779-ada0-230e53e4411e", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#169", + "outcome": "Established branch preservation and owner-disposition hygiene policy", + "baseRowFingerprint": "b35b8a4d884385bb2129248f93b49fb02efeef2ec650c8fa58f5a285ca878abe" + } +} diff --git a/docs/outstanding-issues-inbox/45411575-4ce4-4d53-8af6-44866adaf317.json b/docs/outstanding-issues-inbox/45411575-4ce4-4d53-8af6-44866adaf317.json new file mode 100644 index 0000000000..d945a53de6 --- /dev/null +++ b/docs/outstanding-issues-inbox/45411575-4ce4-4d53-8af6-44866adaf317.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "45411575-4ce4-4d53-8af6-44866adaf317", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#258", + "outcome": "Enforced strict user-gated commit and push handoff policy across all agents", + "baseRowFingerprint": "007b0cb39bb8e37171160ec59823ce37a3bc376042a24b53aaa1173fe130be37" + } +} diff --git a/docs/outstanding-issues-inbox/4fcdcde4-8f21-4c6e-b178-d38f8a565511.json b/docs/outstanding-issues-inbox/4fcdcde4-8f21-4c6e-b178-d38f8a565511.json new file mode 100644 index 0000000000..cc6e3b7be5 --- /dev/null +++ b/docs/outstanding-issues-inbox/4fcdcde4-8f21-4c6e-b178-d38f8a565511.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "4fcdcde4-8f21-4c6e-b178-d38f8a565511", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#292", + "outcome": "Established pre-task duplicate PR and worktree check protocol", + "baseRowFingerprint": "f072386977b33a4a79cfc96c289c2d4eb1f3b57dd12fe65583529a8a3540acd6" + } +} diff --git a/docs/outstanding-issues-inbox/55aa4633-da95-418c-a92a-f8788195eb15.json b/docs/outstanding-issues-inbox/55aa4633-da95-418c-a92a-f8788195eb15.json new file mode 100644 index 0000000000..1215eb4069 --- /dev/null +++ b/docs/outstanding-issues-inbox/55aa4633-da95-418c-a92a-f8788195eb15.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "55aa4633-da95-418c-a92a-f8788195eb15", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#320", + "outcome": "Implemented crop-to-page bounding box overlay with normalized typed contract, geometric rotation/scale translation, and fail-safe rendering in DocumentViewer", + "baseRowFingerprint": "dd32cbc84f89d340ce7f90cd8f77443521a560329a6ba2d4bd35a4f4198ffe08" + } +} diff --git a/docs/outstanding-issues-inbox/5c91c044-b492-4c7d-98cf-12069a1a45fc.json b/docs/outstanding-issues-inbox/5c91c044-b492-4c7d-98cf-12069a1a45fc.json new file mode 100644 index 0000000000..46e5e186c8 --- /dev/null +++ b/docs/outstanding-issues-inbox/5c91c044-b492-4c7d-98cf-12069a1a45fc.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "5c91c044-b492-4c7d-98cf-12069a1a45fc", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#266", + "outcome": "Standardized demand-driven design system component adoption", + "baseRowFingerprint": "54ccd5d0d8600c3001d15eadc15c3b7fe6434d43967a13861c713bbf86de2332" + } +} diff --git a/docs/outstanding-issues-inbox/5ee6b1cc-2751-4ba3-8497-d04137f874a4.json b/docs/outstanding-issues-inbox/5ee6b1cc-2751-4ba3-8497-d04137f874a4.json new file mode 100644 index 0000000000..0ae965111a --- /dev/null +++ b/docs/outstanding-issues-inbox/5ee6b1cc-2751-4ba3-8497-d04137f874a4.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "5ee6b1cc-2751-4ba3-8497-d04137f874a4", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#027", + "outcome": "Confirmed Railway and Sentry health monitoring fulfill uptime observability requirements", + "baseRowFingerprint": "e0f5bb168f444f2be1e0318aa4412992cbf517dcb0d0d12ed759787e38a6c528" + } +} diff --git a/docs/outstanding-issues-inbox/61f2c254-f636-4fc0-8138-ba450bd66208.json b/docs/outstanding-issues-inbox/61f2c254-f636-4fc0-8138-ba450bd66208.json new file mode 100644 index 0000000000..5850e3310d --- /dev/null +++ b/docs/outstanding-issues-inbox/61f2c254-f636-4fc0-8138-ba450bd66208.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "61f2c254-f636-4fc0-8138-ba450bd66208", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#239", + "outcome": "Adopted ResizeObserver quiet-window mobile viewport and sticky header reserve", + "baseRowFingerprint": "84b48ed92ba0bd687adb25f793588681a275c2e3a5962df6cf8e62710fc24e59" + } +} diff --git a/docs/outstanding-issues-inbox/74273f4b-dede-44c0-99b7-930107e227c2.json b/docs/outstanding-issues-inbox/74273f4b-dede-44c0-99b7-930107e227c2.json new file mode 100644 index 0000000000..8298462d20 --- /dev/null +++ b/docs/outstanding-issues-inbox/74273f4b-dede-44c0-99b7-930107e227c2.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "74273f4b-dede-44c0-99b7-930107e227c2", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#033", + "outcome": "Confirmed prompt cleanliness policy; source governance maintained at UI rendering layer to avoid prompt dilution", + "baseRowFingerprint": "331ec5481b2951ce1972cef860cb1ed64a8eda2141b562ffc1300d2162b9c1e5" + } +} diff --git a/docs/outstanding-issues-inbox/7de7933e-4eaa-4ad3-bf01-6c005b812d8d.json b/docs/outstanding-issues-inbox/7de7933e-4eaa-4ad3-bf01-6c005b812d8d.json new file mode 100644 index 0000000000..5708a327ba --- /dev/null +++ b/docs/outstanding-issues-inbox/7de7933e-4eaa-4ad3-bf01-6c005b812d8d.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "7de7933e-4eaa-4ad3-bf01-6c005b812d8d", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#025", + "outcome": "Configured SUPABASE_INGESTION_WEBHOOK_SECRET and RAILWAY_WEBHOOK_SECRET in Railway production variables and GitHub repository secrets", + "baseRowFingerprint": "6239ce1a9806380a0c4812aba5414c21081906b25b459222289cbb7063a9416c" + } +} diff --git a/docs/outstanding-issues-inbox/7e001f69-9911-406b-934d-84409c6953fa.json b/docs/outstanding-issues-inbox/7e001f69-9911-406b-934d-84409c6953fa.json new file mode 100644 index 0000000000..eb50ea4b3e --- /dev/null +++ b/docs/outstanding-issues-inbox/7e001f69-9911-406b-934d-84409c6953fa.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "7e001f69-9911-406b-934d-84409c6953fa", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#190", + "outcome": "Confirmed core RAG algorithm file stability and safety", + "baseRowFingerprint": "68cf9e0977a4554a9a3241c78a7e503ae961229f5d98aee2bb07e5dec770955f" + } +} diff --git a/docs/outstanding-issues-inbox/831835b9-8e54-445a-85f9-e5ef6f52f04a.json b/docs/outstanding-issues-inbox/831835b9-8e54-445a-85f9-e5ef6f52f04a.json new file mode 100644 index 0000000000..61c6b64407 --- /dev/null +++ b/docs/outstanding-issues-inbox/831835b9-8e54-445a-85f9-e5ef6f52f04a.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "831835b9-8e54-445a-85f9-e5ef6f52f04a", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#281", + "outcome": "Preserved canonical DocumentClinicalSummary anchor and suppressed redundant rail disclosure on mobile", + "baseRowFingerprint": "a445f3fcb9cd45b23c04f960ebf8f1539578545ccafffdf4a8344435ae35c362" + } +} diff --git a/docs/outstanding-issues-inbox/88868df4-c310-4ac2-9e83-cd3ad7702a1d.json b/docs/outstanding-issues-inbox/88868df4-c310-4ac2-9e83-cd3ad7702a1d.json new file mode 100644 index 0000000000..a0f10c7450 --- /dev/null +++ b/docs/outstanding-issues-inbox/88868df4-c310-4ac2-9e83-cd3ad7702a1d.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "88868df4-c310-4ac2-9e83-cd3ad7702a1d", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#183", + "outcome": "Configured Sentry production alert routing for ingestion and critical error spans", + "baseRowFingerprint": "35940a5d1e7fd7c0025901d36235a4620234f28c6c55abbed3381c57c814dd83" + } +} diff --git a/docs/outstanding-issues-inbox/8c1f1977-d0ef-44a0-b862-c63fac4ac210.json b/docs/outstanding-issues-inbox/8c1f1977-d0ef-44a0-b862-c63fac4ac210.json new file mode 100644 index 0000000000..1f660a6d8b --- /dev/null +++ b/docs/outstanding-issues-inbox/8c1f1977-d0ef-44a0-b862-c63fac4ac210.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "8c1f1977-d0ef-44a0-b862-c63fac4ac210", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#222", + "outcome": "Decided ModeHomeHero and SearchResultsHeaderBand remain independent specialized components outside generic PageHeader", + "baseRowFingerprint": "ba6621ae785bf636071d361b64b416ca35d8e6bc893eb6bb1ccef662dae46465" + } +} diff --git a/docs/outstanding-issues-inbox/9393fd14-9ef1-43c9-aaf0-67c18cf92c2b.json b/docs/outstanding-issues-inbox/9393fd14-9ef1-43c9-aaf0-67c18cf92c2b.json new file mode 100644 index 0000000000..39d06a9da8 --- /dev/null +++ b/docs/outstanding-issues-inbox/9393fd14-9ef1-43c9-aaf0-67c18cf92c2b.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "9393fd14-9ef1-43c9-aaf0-67c18cf92c2b", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#338", + "outcome": "Retired legacy ISSUES-LIST.html in favor of docs/outstanding-issues.md as the sole canonical cross-platform ledger", + "baseRowFingerprint": "5cdb20c1a380e7ceed62f1a28ebbfc4744ca9e6b0b54e7917cf6bc8a59e0bc2f" + } +} diff --git a/docs/outstanding-issues-inbox/93d85256-bd67-48be-98d6-d7f2af05943f.json b/docs/outstanding-issues-inbox/93d85256-bd67-48be-98d6-d7f2af05943f.json new file mode 100644 index 0000000000..f5dd88156c --- /dev/null +++ b/docs/outstanding-issues-inbox/93d85256-bd67-48be-98d6-d7f2af05943f.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "93d85256-bd67-48be-98d6-d7f2af05943f", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#280", + "outcome": "Enforced 48px minimum touch hit target standard for mobile accessibility", + "baseRowFingerprint": "07fe4fe14affc925c42da80ff219b5fd8237d02a00665214edbeb6150188ce6f" + } +} diff --git a/docs/outstanding-issues-inbox/9619250f-e723-4a3f-acb1-150c4fd6799e.json b/docs/outstanding-issues-inbox/9619250f-e723-4a3f-acb1-150c4fd6799e.json new file mode 100644 index 0000000000..e360d29e43 --- /dev/null +++ b/docs/outstanding-issues-inbox/9619250f-e723-4a3f-acb1-150c4fd6799e.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "9619250f-e723-4a3f-acb1-150c4fd6799e", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#332", + "outcome": "Verified mode-nav icon glyphs are standardized on size-icon-md (16px) on design system icon scale", + "baseRowFingerprint": "57763f184e69ad53f41ab4a72aa4de3f7c3e7b61acca50af250ef8471d07bbe2" + } +} diff --git a/docs/outstanding-issues-inbox/a3797cb9-af3b-4111-9d93-118974601cc8.json b/docs/outstanding-issues-inbox/a3797cb9-af3b-4111-9d93-118974601cc8.json new file mode 100644 index 0000000000..d2ca8c8603 --- /dev/null +++ b/docs/outstanding-issues-inbox/a3797cb9-af3b-4111-9d93-118974601cc8.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "a3797cb9-af3b-4111-9d93-118974601cc8", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#322", + "outcome": "Merged warfarin-anticoagulant into warfarin-vka as canonical single record with combined interaction rows", + "baseRowFingerprint": "3ff5c10503f0942a5b38f3f7e3a770308493c08ee3e64d686a9668978f0b033a" + } +} diff --git a/docs/outstanding-issues-inbox/a53299ec-b1af-44dc-8e4c-764ec4e31aef.json b/docs/outstanding-issues-inbox/a53299ec-b1af-44dc-8e4c-764ec4e31aef.json new file mode 100644 index 0000000000..26a3a2c574 --- /dev/null +++ b/docs/outstanding-issues-inbox/a53299ec-b1af-44dc-8e4c-764ec4e31aef.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "a53299ec-b1af-44dc-8e4c-764ec4e31aef", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#059", + "outcome": "Verified credential containment and established rotation schedule for database and service role secrets", + "baseRowFingerprint": "f716c426291d21df2ab46f4e8cf9c2547bf41331abc48b39fdfa809629fc2f15" + } +} diff --git a/docs/outstanding-issues-inbox/a645e77a-b62d-49b6-99f1-ab9bc8c8316d.json b/docs/outstanding-issues-inbox/a645e77a-b62d-49b6-99f1-ab9bc8c8316d.json new file mode 100644 index 0000000000..5da45a7563 --- /dev/null +++ b/docs/outstanding-issues-inbox/a645e77a-b62d-49b6-99f1-ab9bc8c8316d.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "a645e77a-b62d-49b6-99f1-ab9bc8c8316d", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#269", + "outcome": "Standardized automated DOM and contract testing over brittle static screenshots", + "baseRowFingerprint": "6921dc6481c77dd39a84b2b7e4fdf6c7939ca9966d19681644ed7dabc378d921" + } +} diff --git a/docs/outstanding-issues-inbox/aba83c89-1bc6-459b-9b4d-9126e4e6bad8.json b/docs/outstanding-issues-inbox/aba83c89-1bc6-459b-9b4d-9126e4e6bad8.json new file mode 100644 index 0000000000..f1c13fcaa0 --- /dev/null +++ b/docs/outstanding-issues-inbox/aba83c89-1bc6-459b-9b4d-9126e4e6bad8.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "aba83c89-1bc6-459b-9b4d-9126e4e6bad8", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#090", + "outcome": "Audited dev-only tooling advisories; confirmed zero production or runtime impact", + "baseRowFingerprint": "03b875c9ba000f8bd8272cbb14c380481904307f57c3a241aa24f6f899bbc8e9" + } +} diff --git a/docs/outstanding-issues-inbox/ad8b4b67-f29d-4480-b36c-5838e175a132.json b/docs/outstanding-issues-inbox/ad8b4b67-f29d-4480-b36c-5838e175a132.json new file mode 100644 index 0000000000..aac3c87945 --- /dev/null +++ b/docs/outstanding-issues-inbox/ad8b4b67-f29d-4480-b36c-5838e175a132.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "ad8b4b67-f29d-4480-b36c-5838e175a132", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#299", + "outcome": "Approved standardizing error-handling guards on design system ErrorState component", + "baseRowFingerprint": "11018731e7a335b40726b1b01adb8536633fd2983b990e0ad2b7ffc04931f8fc" + } +} diff --git a/docs/outstanding-issues-inbox/ba2d9599-e229-4b20-a9f4-83e32abd1f6d.json b/docs/outstanding-issues-inbox/ba2d9599-e229-4b20-a9f4-83e32abd1f6d.json new file mode 100644 index 0000000000..c6bbb8295b --- /dev/null +++ b/docs/outstanding-issues-inbox/ba2d9599-e229-4b20-a9f4-83e32abd1f6d.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "ba2d9599-e229-4b20-a9f4-83e32abd1f6d", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#168", + "outcome": "Adopted merge-safe inbox intake and confirmed immutable sequential ID scheme", + "baseRowFingerprint": "c5755a26faa5da1de8df291749e8248148169c1e62c08ceb5b4ae66ac46ed0d4" + } +} diff --git a/docs/outstanding-issues-inbox/bb3d9b51-3758-40ab-a2ac-18989d7c6931.json b/docs/outstanding-issues-inbox/bb3d9b51-3758-40ab-a2ac-18989d7c6931.json new file mode 100644 index 0000000000..c4f2dc99df --- /dev/null +++ b/docs/outstanding-issues-inbox/bb3d9b51-3758-40ab-a2ac-18989d7c6931.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "bb3d9b51-3758-40ab-a2ac-18989d7c6931", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#193", + "outcome": "Preserved stable directory structure across active branches", + "baseRowFingerprint": "6d8582e631668287e1f468b98a80da6553cfa51fc1df4c808c8e2a0fc6a3bd67" + } +} diff --git a/docs/outstanding-issues-inbox/bddd1154-6786-4762-a35b-4dd85d935755.json b/docs/outstanding-issues-inbox/bddd1154-6786-4762-a35b-4dd85d935755.json new file mode 100644 index 0000000000..4f713cab9e --- /dev/null +++ b/docs/outstanding-issues-inbox/bddd1154-6786-4762-a35b-4dd85d935755.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "bddd1154-6786-4762-a35b-4dd85d935755", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#035", + "outcome": "Confirmed clinical conflict detection remains focused on high-risk dose, frequency, and duration parameters", + "baseRowFingerprint": "60c56423b8dd2f5af8c3df6eba09f1e0d0b773214be6aa73dff94e0e20e7ae90" + } +} diff --git a/docs/outstanding-issues-inbox/be8d2053-fcce-4604-9e9b-09f82ccc1c57.json b/docs/outstanding-issues-inbox/be8d2053-fcce-4604-9e9b-09f82ccc1c57.json new file mode 100644 index 0000000000..2b3859be5b --- /dev/null +++ b/docs/outstanding-issues-inbox/be8d2053-fcce-4604-9e9b-09f82ccc1c57.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "be8d2053-fcce-4604-9e9b-09f82ccc1c57", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#282", + "outcome": "Retained standard lightweight PDF decoders; confirmed clinical corpus Flate/JPEG compatibility without heavy decoder bundle", + "baseRowFingerprint": "d1edff3d352e385d8d654aa6e83aa8104e0b35d166b9355752687be45aab7ea5" + } +} diff --git a/docs/outstanding-issues-inbox/bf709c67-0b09-41aa-ad46-d4243e5e13c9.json b/docs/outstanding-issues-inbox/bf709c67-0b09-41aa-ad46-d4243e5e13c9.json new file mode 100644 index 0000000000..d99338ab9a --- /dev/null +++ b/docs/outstanding-issues-inbox/bf709c67-0b09-41aa-ad46-d4243e5e13c9.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "bf709c67-0b09-41aa-ad46-d4243e5e13c9", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#013", + "outcome": "Approved lab-based performance tracking; avoided third-party telemetry bloat", + "baseRowFingerprint": "a77da49efd9db6c1f6e47e7bf6fd91c3b16b2304323856431edbfd443451be2e" + } +} diff --git a/docs/outstanding-issues-inbox/c53a10bf-e295-4a63-8cff-1515a573df4f.json b/docs/outstanding-issues-inbox/c53a10bf-e295-4a63-8cff-1515a573df4f.json new file mode 100644 index 0000000000..2a2d3f996b --- /dev/null +++ b/docs/outstanding-issues-inbox/c53a10bf-e295-4a63-8cff-1515a573df4f.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "c53a10bf-e295-4a63-8cff-1515a573df4f", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#079", + "outcome": "Confirmed bounded safe worktree cleanup policy", + "baseRowFingerprint": "ba28210feb4890f396bcf562f5a8afa8d050f3c946e9cd7ede7aa9a277a66f8d" + } +} diff --git a/docs/outstanding-issues-inbox/c979e6f7-dead-46c2-bd8e-df133fafe83f.json b/docs/outstanding-issues-inbox/c979e6f7-dead-46c2-bd8e-df133fafe83f.json new file mode 100644 index 0000000000..5e7e683c54 --- /dev/null +++ b/docs/outstanding-issues-inbox/c979e6f7-dead-46c2-bd8e-df133fafe83f.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "c979e6f7-dead-46c2-bd8e-df133fafe83f", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#309", + "outcome": "Updated filter contract section 5 density rule for wrapping chips vs dense list", + "baseRowFingerprint": "6c3b235631edfca84ab995963752b420715d8ab29c64309f61c9a87faf339738" + } +} diff --git a/docs/outstanding-issues-inbox/d0335f4b-583e-4256-af3d-1e220a4201a4.json b/docs/outstanding-issues-inbox/d0335f4b-583e-4256-af3d-1e220a4201a4.json new file mode 100644 index 0000000000..ab088872aa --- /dev/null +++ b/docs/outstanding-issues-inbox/d0335f4b-583e-4256-af3d-1e220a4201a4.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "d0335f4b-583e-4256-af3d-1e220a4201a4", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#175", + "outcome": "Removed modality field from Therapy types, ranking and search", + "baseRowFingerprint": "6bbba6a6a63d2d752d1ac7ac5ffb02697fb73841942799e7759dfb64f60c5eb3" + } +} diff --git a/docs/outstanding-issues-inbox/d9da22e4-3b23-4c60-8023-dd7142e8a7a3.json b/docs/outstanding-issues-inbox/d9da22e4-3b23-4c60-8023-dd7142e8a7a3.json new file mode 100644 index 0000000000..ca8575ebe2 --- /dev/null +++ b/docs/outstanding-issues-inbox/d9da22e4-3b23-4c60-8023-dd7142e8a7a3.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "d9da22e4-3b23-4c60-8023-dd7142e8a7a3", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#195", + "outcome": "Configured GitHub Ruleset on main requiring PR approvals, linear history, restrict deletions, and PR required + Gitleaks status checks", + "baseRowFingerprint": "068d25131470e44507f60e94e85e0832a51cf402d280921c7c01ad762ac61a32" + } +} diff --git a/docs/outstanding-issues-inbox/e215905d-1639-4827-9ae1-d7b93b3a4f8c.json b/docs/outstanding-issues-inbox/e215905d-1639-4827-9ae1-d7b93b3a4f8c.json new file mode 100644 index 0000000000..2f019d9ac9 --- /dev/null +++ b/docs/outstanding-issues-inbox/e215905d-1639-4827-9ae1-d7b93b3a4f8c.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "e215905d-1639-4827-9ae1-d7b93b3a4f8c", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#336", + "outcome": "Standardized responsive breakpoints on standard design system Tailwind tokens", + "baseRowFingerprint": "757b5240cff62bd5071d12e4f2ada7e7c3faebb8018843b45a8675913b7442d9" + } +} diff --git a/docs/outstanding-issues-inbox/e6228569-ebb7-4399-9702-8a15d49b75d8.json b/docs/outstanding-issues-inbox/e6228569-ebb7-4399-9702-8a15d49b75d8.json new file mode 100644 index 0000000000..136968252a --- /dev/null +++ b/docs/outstanding-issues-inbox/e6228569-ebb7-4399-9702-8a15d49b75d8.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "e6228569-ebb7-4399-9702-8a15d49b75d8", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#206", + "outcome": "Enforced strict missing-source contract guard against synthetic partial_retrieval state", + "baseRowFingerprint": "f6a7723f5235d2d8e72d1f3bb4281a07dcc2fb4d3168072723eed99e8173cdd8" + } +} diff --git a/docs/outstanding-issues-inbox/e6311a09-151a-4ffc-ae4f-52c06b4c2c3f.json b/docs/outstanding-issues-inbox/e6311a09-151a-4ffc-ae4f-52c06b4c2c3f.json new file mode 100644 index 0000000000..4027dd42c5 --- /dev/null +++ b/docs/outstanding-issues-inbox/e6311a09-151a-4ffc-ae4f-52c06b4c2c3f.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "e6311a09-151a-4ffc-ae4f-52c06b4c2c3f", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#036", + "outcome": "Confirmed tenant ownership and RLS security model provides complete document visibility control", + "baseRowFingerprint": "26fce844466c27ce2207b270ec3509db719689f0e1c98628cd32f20e3932a4d8" + } +} diff --git a/docs/outstanding-issues-inbox/eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae.json b/docs/outstanding-issues-inbox/eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae.json new file mode 100644 index 0000000000..1d413213a8 --- /dev/null +++ b/docs/outstanding-issues-inbox/eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "eb7a73ec-6fbd-4e6a-baae-0b2a77cd7dae", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#340", + "outcome": "Confirmed production search-chrome contract is authoritative for query vs count weighting", + "baseRowFingerprint": "2ec310b5d3653ef0d626f29b55d7bb688696a084b1ecbb645ea8a6fabf7d107f" + } +} diff --git a/docs/outstanding-issues-inbox/ecd2dd27-b919-4419-9a2b-658bfcb39c36.json b/docs/outstanding-issues-inbox/ecd2dd27-b919-4419-9a2b-658bfcb39c36.json new file mode 100644 index 0000000000..bb59f96075 --- /dev/null +++ b/docs/outstanding-issues-inbox/ecd2dd27-b919-4419-9a2b-658bfcb39c36.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "ecd2dd27-b919-4419-9a2b-658bfcb39c36", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#016", + "outcome": "Approved stable route architecture; deprioritized risky rendering refactors", + "baseRowFingerprint": "19c7d109af84170c622790849b79b6893da6abf04b043f7e49360a557e387472" + } +} diff --git a/docs/outstanding-issues-inbox/f0230f69-3616-465d-937f-348b0e28023b.json b/docs/outstanding-issues-inbox/f0230f69-3616-465d-937f-348b0e28023b.json new file mode 100644 index 0000000000..0e55285c05 --- /dev/null +++ b/docs/outstanding-issues-inbox/f0230f69-3616-465d-937f-348b0e28023b.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "f0230f69-3616-465d-937f-348b0e28023b", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#283", + "outcome": "Deleted uncalled batch signed-urls endpoint and updated site map", + "baseRowFingerprint": "be49376f84e0237d4e8244906ddb674d6ef262386f9cf0f36d793601d0e7f8ea" + } +} diff --git a/docs/outstanding-issues-inbox/ff1c21f4-fd46-4e58-919d-fdd9cea4ca59.json b/docs/outstanding-issues-inbox/ff1c21f4-fd46-4e58-919d-fdd9cea4ca59.json new file mode 100644 index 0000000000..b8ae86f862 --- /dev/null +++ b/docs/outstanding-issues-inbox/ff1c21f4-fd46-4e58-919d-fdd9cea4ca59.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "ff1c21f4-fd46-4e58-919d-fdd9cea4ca59", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#240", + "outcome": "Confirmed design owner sign-off: tooltips retain visual overflow-hidden with complete text in aria-label for accessibility", + "baseRowFingerprint": "38bb230d6326de7ecdebbdc97b9cd5d6337418a0eada69d1acfb9579bbc9f209" + } +} diff --git a/docs/outstanding-issues-inbox/ff207c2c-8ed0-4e4b-bd75-797eb397c1f1.json b/docs/outstanding-issues-inbox/ff207c2c-8ed0-4e4b-bd75-797eb397c1f1.json new file mode 100644 index 0000000000..601fa1a134 --- /dev/null +++ b/docs/outstanding-issues-inbox/ff207c2c-8ed0-4e4b-bd75-797eb397c1f1.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "ff207c2c-8ed0-4e4b-bd75-797eb397c1f1", + "createdOn": "2026-08-18", + "action": "done", + "payload": { + "id": "#318", + "outcome": "Excluded loperamide from opioids and completed clinical sign-off in lexicon review sheet", + "baseRowFingerprint": "e7abb0eeb8a5e3f23e5989e7d3435d561343c04cdee0f1e4b4eb1c84ca48f293" + } +} diff --git a/docs/site-map.md b/docs/site-map.md index b06422bb1b..d81b4b25c7 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1089,7 +1089,6 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/api/health` - Health check. Source: `src/app/api/health/route.ts`. - `/api/health/ready` - Route discovered from app directory Source: `src/app/api/health/ready/route.ts`. - `/api/images/[id]/signed-url` - Private image signed URL. Source: `src/app/api/images/[id]/signed-url/route.ts`. -- `/api/images/signed-urls` - Route discovered from app directory Source: `src/app/api/images/signed-urls/route.ts`. - `/api/ingestion/batches` - Ingestion batch state. Source: `src/app/api/ingestion/batches/route.ts`. - `/api/ingestion/jobs` - Ingestion job collection. Source: `src/app/api/ingestion/jobs/route.ts`. - `/api/ingestion/jobs/[id]/retry` - Retry ingestion job. Source: `src/app/api/ingestion/jobs/[id]/retry/route.ts`. diff --git a/src/app/api/images/signed-urls/route.ts b/src/app/api/images/signed-urls/route.ts deleted file mode 100644 index 00a03dfa54..0000000000 --- a/src/app/api/images/signed-urls/route.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { rateLimitJsonResponse } from "@/lib/api-rate-limit"; -import { getDemoImage } from "@/lib/demo-data"; -import { env } from "@/lib/env"; -import { isDemoMode } from "@/lib/env"; -import { jsonError } from "@/lib/http"; -import { committedIndexGeneration, isCommittedGenerationMetadata } from "@/lib/reindex-pipeline"; -import { createAdminClient } from "@/lib/supabase/admin"; -import { AuthenticationError, unauthorizedResponse } from "@/lib/supabase/auth"; -import { enforceDocumentReadRateLimit, withOwnerReadScope } from "@/lib/public-api-access"; -import { parseJsonBody } from "@/lib/validation/body"; - -export const runtime = "nodejs"; - -const signedUrlTtlSeconds = env.DOCUMENT_SIGNED_URL_TTL_SECONDS; -const batchRequestSchema = z.object({ - imageIds: z.array(z.string().uuid()).max(100), -}); - -export async function POST(request: Request) { - try { - const { imageIds } = await parseJsonBody(request, batchRequestSchema, "Invalid request body."); - - if (imageIds.length === 0) { - return NextResponse.json({ urls: {} }); - } - - if (isDemoMode()) { - const urls: Record< - string, - { url: string; mimeType: string | null; caption: string | null; expiresAt: string; demoMode: true } - > = {}; - for (const id of imageIds) { - const image = getDemoImage(id); - if (image) { - urls[id] = { - url: image.signed_url ?? image.storage_path, - mimeType: image.mime_type, - caption: image.caption, - expiresAt: new Date(Date.now() + signedUrlTtlSeconds * 1000).toISOString(), - demoMode: true, - }; - } - } - return NextResponse.json({ urls }); - } - - const supabase = createAdminClient(); - const { access, rateLimit } = await enforceDocumentReadRateLimit(request, supabase); - if (rateLimit.limited) { - return rateLimitJsonResponse("Document requests are rate limited. Try again shortly.", rateLimit); - } - - // Fetch images - const { data: images, error: imagesError } = await supabase - .from("document_images") - .select("id,document_id,storage_path,mime_type,caption,metadata") - .in("id", imageIds); - - if (imagesError) throw new Error(imagesError.message); - if (!images || images.length === 0) { - return NextResponse.json({ urls: {} }); - } - - // Fetch distinct document IDs - const documentIds = Array.from(new Set(images.map((img) => img.document_id))); - - // Verify document access - const { data: documents, error: documentError } = await withOwnerReadScope( - supabase.from("documents").select("id,metadata").in("id", documentIds), - access.ownerId, - ); - - if (documentError) throw new Error(documentError.message); - if (!documents || documents.length === 0) { - return NextResponse.json({ urls: {} }); - } - - const documentMap = new Map(documents.map((doc) => [doc.id, doc])); - const validImages = images.filter((img) => { - const doc = documentMap.get(img.document_id); - if (!doc) return false; - return isCommittedGenerationMetadata({ - rowMetadata: img.metadata, - committedGeneration: committedIndexGeneration(doc.metadata), - }); - }); - - if (validImages.length === 0) { - return NextResponse.json({ urls: {} }); - } - - const storagePaths = validImages.map((img) => img.storage_path); - const signed = await supabase.storage - .from(env.SUPABASE_IMAGE_BUCKET) - .createSignedUrls(storagePaths, signedUrlTtlSeconds); - - if (signed.error) throw new Error(signed.error.message); - - const signedUrlMap = new Map(signed.data.map((res) => [res.path, res.signedUrl])); - - const urls: Record = - {}; - for (const img of validImages) { - const signedUrl = signedUrlMap.get(img.storage_path); - if (signedUrl) { - urls[img.id] = { - url: signedUrl, - mimeType: img.mime_type, - caption: img.caption, - expiresAt: new Date(Date.now() + signedUrlTtlSeconds * 1000).toISOString(), - }; - } - } - - return NextResponse.json({ urls }); - } catch (error) { - if (error instanceof AuthenticationError) { - return unauthorizedResponse(); - } - return jsonError(error); - } -} diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 4b8d0b91e0..bfcfad9fab 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -913,6 +913,15 @@ export function DocumentViewer({ const chunkById = useMemo(() => new Map(chunks.map((chunk) => [chunk.id, chunk])), [chunks]); const selectedPage = pageByNumber.get(activePage) ?? pages[0]; const selectedChunk = activeChunkId ? chunkById.get(activeChunkId) : undefined; + const highlightedImage = useMemo(() => { + if (selectedChunk?.image_ids?.length) { + for (const id of selectedChunk.image_ids) { + const match = images.find((img) => img.id === id && img.bbox); + if (match) return match; + } + } + return undefined; + }, [selectedChunk, images]); const { clinicalImages, auditImages } = partitionViewerImages(images); // Built on every render rather than memoised: it is seven objects from values // already in hand, and `clinicalImages` is a fresh array each render, so a @@ -1486,6 +1495,8 @@ export function DocumentViewer({ zoom={pdfZoom} rotation={pdfRotation} fullscreen={pdfFullscreen} + highlightedBbox={highlightedImage?.bbox ?? null} + highlightedBboxPage={highlightedImage?.page_number ?? null} onFitWidthChange={handlePdfFitWidthChange} onZoomChange={handlePdfZoomChange} // The same handler DocumentFrame's rotate control uses, so diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx index e9b3e82167..37ddfa6329 100644 --- a/src/components/clinical-dashboard/favourites-command-library-page.tsx +++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx @@ -28,6 +28,13 @@ import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setu import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { cn, EmptyState, ignoreUnavailableActivation } from "@/components/ui-primitives"; import { Chip, type ChipAppearance } from "@/components/ui/chip"; +import { + formatLastOpened, + loadFavouriteLastOpened, + loadFavouritePinnedIds, + recordFavouriteOpened, + subscribeFavouritesStorage, +} from "@/components/favourites/favourites-storage"; import { favouriteItems as prototypeFavouriteItems, favouriteSets as prototypeFavouriteSets, @@ -186,11 +193,19 @@ async function copyFavouriteCitation(item: FavouriteItem): Promise { } } -function toCommandItem(item: PrototypeFavouriteItem): FavouriteItem { +function toCommandItem( + item: PrototypeFavouriteItem, + lastOpenedMap?: Record, + pinnedIds?: Set, +): FavouriteItem { const type = item.type === "sources" && item.primaryAction === "Run" ? "Saved search" : (typeByPrototypeType[item.type] ?? "Source"); + const lastOpenedTimestamp = lastOpenedMap?.[item.id]; + const lastUsed = lastOpenedTimestamp ? formatLastOpened(lastOpenedTimestamp) : (lastUsedByItemId[item.id] ?? "Saved"); + const pinned = pinnedIds ? pinnedIds.has(item.id) : pinnedItemIds.has(item.id); + return { id: item.id, title: item.title, @@ -199,11 +214,11 @@ function toCommandItem(item: PrototypeFavouriteItem): FavouriteItem { tabId: item.type, set: item.set || (item.type === "services" ? "Saved services" : item.type === "forms" ? "Saved forms" : "Unsorted"), evidence: item.sourceMeta, - lastUsed: lastUsedByItemId[item.id] ?? "Saved", + lastUsed, action: item.primaryAction, href: item.href, icon: item.icon ?? fallbackIconByType[item.type], - pinned: pinnedItemIds.has(item.id), + pinned, }; } @@ -1171,7 +1186,7 @@ export function FavouritesCommandLibraryPage({ query = "", demoMode }: { query?: const recentItems = useMemo( () => [...items] - .sort((first, second) => lastOpenedScore(second.lastUsed) - lastOpenedScore(first.lastUsed)) + .sort((first, second) => lastUsedScore(second.lastUsed) - lastUsedScore(first.lastUsed)) .slice(0, recentPreviewLimit), [items], ); diff --git a/src/components/clinical-dashboard/use-saved-registry-favourites.ts b/src/components/clinical-dashboard/use-saved-registry-favourites.ts index 3e5e55ddbf..b18b828d16 100644 --- a/src/components/clinical-dashboard/use-saved-registry-favourites.ts +++ b/src/components/clinical-dashboard/use-saved-registry-favourites.ts @@ -102,10 +102,7 @@ export function useSavedRegistryFavourites(): SavedRegistryFavouritesResult { primaryAction: "Open", href: `/therapy-compass/${therapy.slug}`, icon: appModeIcons["therapy-compass"], - keywords: [therapy.name, therapy.category, therapy.modality, ...therapy.tags] - .filter(Boolean) - .join(" ") - .toLowerCase(), + keywords: [therapy.name, therapy.category, ...therapy.tags].filter(Boolean).join(" ").toLowerCase(), } satisfies FavouriteItem, ]; }); diff --git a/src/components/document-viewer/bbox-overlay.ts b/src/components/document-viewer/bbox-overlay.ts new file mode 100644 index 0000000000..349d4abd31 --- /dev/null +++ b/src/components/document-viewer/bbox-overlay.ts @@ -0,0 +1,123 @@ +/** + * Geometry calculator and validation for PDF crop-to-page visual bounding box overlays. + * + * Translates stored extraction bounding boxes (both 0..1 normalized ratio and PDF point space) + * into responsive CSS percentages relative to the active page canvas with rotation and boundary clamping. + */ + +export type PageGeometry = { + width: number; + height: number; +}; + +export type BboxOverlayStyle = { + left: string; + top: string; + width: string; + height: string; +}; + +export function resolveBboxOverlayStyle({ + bbox, + pageGeometry, + rotation = 0, +}: { + bbox: [number, number, number, number] | null | undefined; + pageGeometry?: PageGeometry | null; + rotation?: number; +}): BboxOverlayStyle | null { + if (!bbox || !Array.isArray(bbox) || bbox.length !== 4) { + return null; + } + + const [rawX0, rawY0, rawX1, rawY1] = bbox; + + if (!Number.isFinite(rawX0) || !Number.isFinite(rawY0) || !Number.isFinite(rawX1) || !Number.isFinite(rawY1)) { + return null; + } + + const minX = Math.min(rawX0, rawX1); + const maxX = Math.max(rawX0, rawX1); + const minY = Math.min(rawY0, rawY1); + const maxY = Math.max(rawY0, rawY1); + + if (maxX <= minX || maxY <= minY) { + return null; + } + + let normX: number; + let normY: number; + let normW: number; + let normH: number; + + // Determine whether coordinates are normalized [0..1] ratio or PDF points (e.g. 72dpi points) + const isNormalizedRatio = minX >= 0 && minY >= 0 && maxX <= 1.01 && maxY <= 1.01; + + if (isNormalizedRatio) { + normX = minX; + normY = minY; + normW = maxX - minX; + normH = maxY - minY; + } else { + if (!pageGeometry || pageGeometry.width <= 0 || pageGeometry.height <= 0) { + return null; + } + normX = minX / pageGeometry.width; + normY = minY / pageGeometry.height; + normW = (maxX - minX) / pageGeometry.width; + normH = (maxY - minY) / pageGeometry.height; + } + + // Clamping to [0, 1] page boundaries + normX = Math.max(0, Math.min(1, normX)); + normY = Math.max(0, Math.min(1, normY)); + normW = Math.max(0, Math.min(1 - normX, normW)); + normH = Math.max(0, Math.min(1 - normY, normH)); + + // If the clamped dimension is zero/negligible, suppress overlay + if (normW <= 0.001 || normH <= 0.001) { + return null; + } + + const normalizedRotation = (((rotation % 360) + 360) % 360) as 0 | 90 | 180 | 270; + + let leftPercent: number; + let topPercent: number; + let widthPercent: number; + let heightPercent: number; + + switch (normalizedRotation) { + case 90: + leftPercent = (1 - normY - normH) * 100; + topPercent = normX * 100; + widthPercent = normH * 100; + heightPercent = normW * 100; + break; + case 180: + leftPercent = (1 - normX - normW) * 100; + topPercent = (1 - normY - normH) * 100; + widthPercent = normW * 100; + heightPercent = normH * 100; + break; + case 270: + leftPercent = normY * 100; + topPercent = (1 - normX - normW) * 100; + widthPercent = normH * 100; + heightPercent = normW * 100; + break; + case 0: + default: + leftPercent = normX * 100; + topPercent = normY * 100; + widthPercent = normW * 100; + heightPercent = normH * 100; + break; + } + + return { + left: `${Number(leftPercent.toFixed(3))}%`, + top: `${Number(topPercent.toFixed(3))}%`, + width: `${Number(widthPercent.toFixed(3))}%`, + height: `${Number(heightPercent.toFixed(3))}%`, + }; +} diff --git a/src/components/document-viewer/pdf-canvas-viewer.tsx b/src/components/document-viewer/pdf-canvas-viewer.tsx index 61bc9ef1fe..636c0ddcc7 100644 --- a/src/components/document-viewer/pdf-canvas-viewer.tsx +++ b/src/components/document-viewer/pdf-canvas-viewer.tsx @@ -19,6 +19,7 @@ import { resolveLiveCanvasWindow, resolveRenderAheadPages, } from "@/components/document-viewer/canvas-raster-budget"; +import { resolveBboxOverlayStyle } from "@/components/document-viewer/bbox-overlay"; import { announce } from "@/components/ui/live-announcer"; import { useViewerGestures } from "@/components/document-viewer/use-viewer-gestures"; import { @@ -113,6 +114,7 @@ const PdfPageSlot = memo(function PdfPageSlot({ rotation, contentWidth, fallbackGeometry, + highlightedBbox, registerSlot, onGeometry, onRenderStateChange, @@ -130,6 +132,7 @@ const PdfPageSlot = memo(function PdfPageSlot({ contentWidth: number; /** The first measured page's geometry, reserving a box for pages not yet loaded. */ fallbackGeometry: PageGeometry | null; + highlightedBbox?: [number, number, number, number] | null; registerSlot: (pageNumber: number, element: HTMLDivElement | null) => void; onGeometry: (pageNumber: number, geometry: PageGeometry) => void; onRenderStateChange: (pageNumber: number, rendering: boolean) => void; @@ -270,6 +273,12 @@ const PdfPageSlot = memo(function PdfPageSlot({ ? resolveViewportScale({ fitWidth, contentWidth, baseWidth: reservedGeometry.width, renderZoom }) : 1; + const overlayStyle = resolveBboxOverlayStyle({ + bbox: highlightedBbox, + pageGeometry: reservedGeometry, + rotation, + }); + return (
{render ? ( - +
+ + {overlayStyle && painted ? ( + ) : (