From 3bc391dff3eb735c3a4b9a5f66b475beeacedd18 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:29:38 +0800 Subject: [PATCH 1/4] chore: prune dead prototype and mockup exports (IMP-04) --- .../calculator-mockups/calculator-fixtures.ts | 4 +-- .../calculator-mockups/calculator-ui.tsx | 24 +++++++-------- src/components/factsheets/factsheets-data.ts | 14 ++++----- src/components/factsheets/factsheets-icons.ts | 6 ++-- src/components/ui-primitives.tsx | 30 +++++++++---------- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/components/calculator-mockups/calculator-fixtures.ts b/src/components/calculator-mockups/calculator-fixtures.ts index 707da5c54..2139015bc 100644 --- a/src/components/calculator-mockups/calculator-fixtures.ts +++ b/src/components/calculator-mockups/calculator-fixtures.ts @@ -15,7 +15,7 @@ import { export type CalculatorTone = "success" | "info" | "warning" | "danger"; -export type CalculatorOption = { +type CalculatorOption = { label: string; /** Compact label for segmented controls on narrow screens. */ short: string; @@ -622,7 +622,7 @@ export const calculators: CalculatorFixture[] = [ }, ]; -export const calculatorById = (id: string): CalculatorFixture => { +const calculatorById = (id: string): CalculatorFixture => { const found = calculators.find((calc) => calc.id === id); if (!found) throw new Error(`Unknown calculator fixture: ${id}`); return found; diff --git a/src/components/calculator-mockups/calculator-ui.tsx b/src/components/calculator-mockups/calculator-ui.tsx index f27a241d6..823ad5853 100644 --- a/src/components/calculator-mockups/calculator-ui.tsx +++ b/src/components/calculator-mockups/calculator-ui.tsx @@ -26,7 +26,7 @@ export function itemScore(item: CalculatorItem, selection: number | undefined): } /** True for scales whose every item is a yes/no checkbox (CAGE, SAD PERSONS). */ -export function isCheckboxOnly(calc: CalculatorFixture): boolean { +function isCheckboxOnly(calc: CalculatorFixture): boolean { return calc.items.length > 0 && calc.items.every((item) => item.kind === "checkbox"); } @@ -36,7 +36,7 @@ export function isCheckboxOnly(calc: CalculatorFixture): boolean { * CAGE/SAD PERSONS screen reads as a valid 0 result (started + complete) only * once the user chooses to record it, not merely by opening the scale. */ -export function seedCheckboxDefaults(calc: CalculatorFixture, answers: AnswerMap): AnswerMap { +function seedCheckboxDefaults(calc: CalculatorFixture, answers: AnswerMap): AnswerMap { if (!isCheckboxOnly(calc)) return answers; if (calc.items.every((item) => answers[item.id] !== undefined)) return answers; const next: AnswerMap = { ...answers }; @@ -46,13 +46,13 @@ export function seedCheckboxDefaults(calc: CalculatorFixture, answers: AnswerMap return next; } -export type CalculatorResult = { +type CalculatorResult = { label: string; tone: CalculatorTone; guidance: string; }; -export type CalculatorState = { +type CalculatorState = { answers: AnswerMap; score: number; /** Options-style items answered so far. */ @@ -157,17 +157,17 @@ export function deriveCalculator(calc: CalculatorFixture, answers: AnswerMap): D }; } -export function toggleCheckboxAnswer(answers: AnswerMap, itemId: string): AnswerMap { +function toggleCheckboxAnswer(answers: AnswerMap, itemId: string): AnswerMap { // Toggle between explicit 1 ("Yes") and 0 ("No") rather than clearing to // undefined, so an unticked box stays a recorded negative answer. return { ...answers, [itemId]: answers[itemId] === 1 ? 0 : 1 }; } -export function selectOptionAnswer(answers: AnswerMap, itemId: string, optionIndex: number): AnswerMap { +function selectOptionAnswer(answers: AnswerMap, itemId: string, optionIndex: number): AnswerMap { return { ...answers, [itemId]: answers[itemId] === optionIndex ? undefined : optionIndex }; } -export function useCalculatorState(calc: CalculatorFixture): CalculatorState { +function useCalculatorState(calc: CalculatorFixture): CalculatorState { const [answers, setAnswers] = useState({}); const toggleCheckbox = useCallback((itemId: string) => { @@ -188,7 +188,7 @@ export function useCalculatorState(calc: CalculatorFixture): CalculatorState { /* ---------- tone styling ---------- */ -export const toneChip: Record = { +const toneChip: Record = { success: "border-[color:var(--success-border)] bg-[color:var(--success-soft)] text-[color:var(--success)]", info: "border-[color:var(--info-border)] bg-[color:var(--info-soft)] text-[color:var(--info)]", warning: "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]", @@ -294,7 +294,7 @@ export function BandLegend({ calc, activeBand }: { calc: CalculatorFixture; acti /* ---------- interactive item controls ---------- */ -export function CheckboxRow({ +function CheckboxRow({ item, checked, onToggle, @@ -357,7 +357,7 @@ export function CheckboxRow({ ); } -export function OptionScale({ +function OptionScale({ item, value, onSelect, @@ -547,14 +547,14 @@ export function formatResultSummary(calc: CalculatorFixture, state: DerivedCalcu * carry bespoke option sets (then items render stacked full labels instead * of numbered chips plus one response key). */ -export function sharedOptionKey(calc: CalculatorFixture) { +function sharedOptionKey(calc: CalculatorFixture) { const optionItems = calc.items.filter((item) => item.kind === "options"); if (optionItems.length < 2) return null; const first = optionItems[0].options; return optionItems.every((item) => item.options === first) ? (first ?? null) : null; } -export function ResponseKey({ calc }: { calc: CalculatorFixture }) { +function ResponseKey({ calc }: { calc: CalculatorFixture }) { const key = sharedOptionKey(calc); if (!key) return null; return ( diff --git a/src/components/factsheets/factsheets-data.ts b/src/components/factsheets/factsheets-data.ts index 77e769dbc..5004f536a 100644 --- a/src/components/factsheets/factsheets-data.ts +++ b/src/components/factsheets/factsheets-data.ts @@ -23,7 +23,7 @@ export const FACTSHEET_DEMO_NOTICE = export type FactsheetCategory = "Medications" | "Conditions" | "Therapies" | "Tests & procedures"; -export type FactsheetKind = "medRich" | "medLite" | "condition" | "therapy" | "procedure"; +type FactsheetKind = "medRich" | "medLite" | "condition" | "therapy" | "procedure"; export type FactsheetIconKey = | "capsule" @@ -38,7 +38,7 @@ export type FactsheetIconKey = | "pill" | "heart"; -export type FactsheetSource = { +type FactsheetSource = { n: string; title: string; org: string; @@ -71,7 +71,7 @@ type FactsheetBase = { sources: FactsheetSource[]; }; -export type MedRichContent = { +type MedRichContent = { kind: "medRich"; keyFacts: Array<{ k: string; v: string }>; whatEasy: string; @@ -82,13 +82,13 @@ export type MedRichContent = { urgentHelp: string; }; -export type MedLiteContent = { +type MedLiteContent = { kind: "medLite"; timing: string; sections: Array<{ heading: string; body: string }>; }; -export type ConditionContent = { +type ConditionContent = { kind: "condition"; intro: string; signs: string[]; @@ -97,14 +97,14 @@ export type ConditionContent = { support: string; }; -export type TherapyContent = { +type TherapyContent = { kind: "therapy"; intro: string; steps: Array<{ n: string; h: string; t: string }>; expect: Array<{ k: string; v: string }>; }; -export type ProcedureContent = { +type ProcedureContent = { kind: "procedure"; why: string; prepare: string[]; diff --git a/src/components/factsheets/factsheets-icons.ts b/src/components/factsheets/factsheets-icons.ts index a7492dc64..026474510 100644 --- a/src/components/factsheets/factsheets-icons.ts +++ b/src/components/factsheets/factsheets-icons.ts @@ -18,7 +18,7 @@ import { createElement } from "react"; import type { FactsheetCategory, FactsheetIconKey } from "@/components/factsheets/factsheets-data"; /** Stable icon-key → Lucide component map for per-sheet icons. */ -export const factsheetIcons: Record = { +const factsheetIcons: Record = { capsule: Pill, pill: Pill, layers: Layers, @@ -33,14 +33,14 @@ export const factsheetIcons: Record = { }; /** Category browse icons for the home topic pills. */ -export const factsheetCategoryIcons: Record = { +const factsheetCategoryIcons: Record = { Medications: Pill, Conditions: BrainCircuit, Therapies: MessagesSquare, "Tests & procedures": ClipboardList, }; -export function factsheetIcon(key: FactsheetIconKey): LucideIcon { +function factsheetIcon(key: FactsheetIconKey): LucideIcon { return factsheetIcons[key]; } diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index c546c5ec8..d9998684f 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -17,9 +17,9 @@ export function cn(...classes: Array) { export const textMuted = "text-[color:var(--text-muted)]"; export const raisedCard = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)]"; -export const insetCard = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-inset)]"; +const insetCard = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-inset)]"; export const appBackdrop = "app-edge-backdrop"; -export const glassPanel = +const glassPanel = "rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] shadow-[var(--shadow-soft)]"; export const glassOverlaySurface = "border border-[color:var(--border-lux)] ring-1 ring-[color:var(--surface-highlight)] backdrop-blur-xl"; @@ -28,11 +28,11 @@ export const panelSubtle = "rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] shadow-[var(--shadow-tight)]"; export const sourceCard = `${panelSubtle} transition hover:border-[color:var(--border-strong)] hover:shadow-[var(--shadow-hover)]`; export const answerSurface = "rounded-lg bg-transparent"; -export const evidenceSurface = +const evidenceSurface = "rounded-lg border border-[color:var(--border)] border-l-[3px] border-l-[color:var(--clinical-accent)] bg-[color:var(--surface-raised)] shadow-[var(--shadow-tight)]"; export const panel = "rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-soft)] ring-1 ring-[color:var(--border-strong)]/20 dark:ring-[color:var(--border-strong)]/10"; -export const controlBase = +const controlBase = "inline-flex min-h-tap items-center justify-center gap-2 rounded-lg text-sm font-semibold transition active:translate-y-px focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:shadow-none"; export const primaryControl = `${controlBase} bg-[color:var(--command)] px-5 text-[color:var(--command-contrast)] shadow-[var(--shadow-tight)] hover:bg-[color:var(--command-hover)] hover:shadow-[var(--shadow-hover)]`; export const floatingControl = @@ -56,21 +56,21 @@ export const metadataPill = export const subtleStatusPill = "inline-flex min-h-7 items-center rounded-md border border-[color:var(--border)] bg-[color:var(--surface-wash)] px-2 text-xs font-semibold text-[color:var(--text-muted)]"; export const clinicalDivider = "border-t border-[color:var(--border)]/80"; -export const iconTile = +const iconTile = "grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; export const iconTilePremium = "grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; -export const compactMetadataRow = +const compactMetadataRow = "mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs font-semibold tabular-nums text-[color:var(--text-muted)]"; -export const sheetSurface = +const sheetSurface = "rounded-t-xl border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-lux)] ring-1 ring-[color:var(--border-strong)]/20 backdrop-blur-xl dark:ring-[color:var(--border-strong)]/10 sm:rounded-lg"; -export const sheetHandle = "mx-auto block h-1 w-10 rounded-full bg-[color:var(--border-strong)]/70 sm:hidden"; +const sheetHandle = "mx-auto block h-1 w-10 rounded-full bg-[color:var(--border-strong)]/70 sm:hidden"; // Comfortable reading measure for long-form prose (answers, source passages, document text). export const proseMeasure = "max-w-[68ch]"; // Geist Mono for clinical codes and identifiers: citation/source indices, page and // chunk numbers, guideline versions, document IDs. Pairs with tabular figures. export const codeText = "font-mono tabular-nums tracking-tight"; -export const commandInput = +const commandInput = "min-h-12 w-full rounded-lg border border-[color:var(--border)]/70 bg-[color:var(--surface)] pl-12 pr-12 text-sm font-semibold text-[color:var(--text)] shadow-[var(--shadow-soft),var(--shadow-inset)] outline-none transition placeholder:text-[color:var(--text-soft)] focus:border-[color:var(--focus)] motion-safe:transition sm:text-base"; export const chatAnswerText = @@ -90,15 +90,15 @@ export const sourceCapsule = "source-capsule-face inline-flex items-center gap-1.5 rounded-full border bg-[color-mix(in_srgb,var(--clinical-accent-soft)_55%,var(--surface))] px-2.5 py-1 text-2xs font-medium text-[color:var(--clinical-accent)]"; export const sourceCapsuleCountBadge = "nums inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-[color:var(--surface-raised)] px-1 text-3xs font-semibold leading-none text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]"; -export const evidenceRow = +const evidenceRow = "flex min-h-12 w-full items-center justify-between gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -export const clinicalNotesRow = +const clinicalNotesRow = "flex min-h-12 w-full items-center justify-between gap-3 rounded-lg border border-[color:var(--clinical-chat-sand-border)] bg-[color:var(--clinical-chat-sand)] px-3 py-2 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-chat-sand-border-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; /* Composer chrome has one owner: the unlayered classes in globals.css. These * exports are semantic handles only, so recipes and cascade rules cannot fight * over input/button dimensions, states, or paint. */ export const chatComposerShellBase = "chat-composer-shell-base"; -export const chatComposerShellDelta = "chat-composer-shell-delta"; +const chatComposerShellDelta = "chat-composer-shell-delta"; export const chatComposerShell = `${chatComposerShellBase} ${chatComposerShellDelta}`; export const chatComposerInput = "chat-composer-input"; export const chatComposerIconButton = "chat-composer-icon-button"; @@ -111,9 +111,9 @@ export const tableMicroActionRow = "flex min-h-tap flex-wrap items-center gap-1 border-t border-[color:var(--border)] px-2 py-1.5 text-xs font-semibold text-[color:var(--text-muted)] sm:min-h-9"; export const sidebarItem = "flex min-h-tap min-w-0 w-full items-center gap-2 overflow-hidden rounded-lg px-2.5 text-sm font-semibold text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] disabled:cursor-not-allowed disabled:opacity-50"; -export const sidebarToolTile = +const sidebarToolTile = "grid min-h-16 place-items-center gap-1 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-2 py-2 text-center text-xs font-semibold text-[color:var(--text)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -export const statusDotBase = "inline-block h-2 w-2 shrink-0 rounded-full"; +const statusDotBase = "inline-block h-2 w-2 shrink-0 rounded-full"; export const statusDotReady = `${statusDotBase} bg-[color:var(--success)]`; export const statusDotReview = `${statusDotBase} bg-[color:var(--warning)]`; export const statusDotMuted = `${statusDotBase} bg-[color:var(--text-soft)]`; @@ -125,7 +125,7 @@ export const toneDanger = export const toneInfo = "border-[color:var(--info-border)] bg-[color:var(--info-soft)] text-[color:var(--info)]"; export const toneWarning = "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; -export const toneWarningQuiet = +const toneWarningQuiet = "border-[color:var(--warning-border)]/60 bg-[color:var(--warning-soft)]/45 text-[color:var(--warning)]"; export const toneNeutral = "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]"; From 8b8639113925601e1687bfe4f1f29c44a4308b61 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:53:50 +0800 Subject: [PATCH 2/4] chore: remediate audit maintainability constraints - Decompose ClinicalDashboard.tsx by extracting notices - Lazy-load admin tools and UploadPanel via next/dynamic - Extract indexing-v3-agent string tools to utils.ts to meet 2191 line budget - Fix check-github-action-pins.mjs syntax duplication --- scripts/check-github-action-pins.mjs | 14 +- scripts/decompose-indexing-v3.mjs | 68 +++++ src/components/ClinicalDashboard.tsx | 73 ++--- .../clinical-dashboard/dashboard-notices.tsx | 60 ++++ supabase/functions/indexing-v3-agent/index.ts | 260 ++--------------- supabase/functions/indexing-v3-agent/utils.ts | 262 ++++++++++++++++++ 6 files changed, 435 insertions(+), 302 deletions(-) create mode 100644 scripts/decompose-indexing-v3.mjs create mode 100644 src/components/clinical-dashboard/dashboard-notices.tsx create mode 100644 supabase/functions/indexing-v3-agent/utils.ts diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs index 539f90a88..ab71a6623 100644 --- a/scripts/check-github-action-pins.mjs +++ b/scripts/check-github-action-pins.mjs @@ -173,19 +173,7 @@ if (!/^ image: semgrep\/semgrep@sha256:[0-9a-f]{64}\s*$/m.test(semgrepGateJ // the per-line validation above only covers workflows, a composite skew (e.g. // setup-node v5 vs v7) was previously invisible. Assert each action name resolves // to a single SHA everywhere it is used. -function discoverCompositeActionFiles(workflowRoot) { - const actionsRoot = path.join(workflowRoot, ".github", "actions"); - if (!existsSync(actionsRoot)) return []; - const files = []; - for (const entry of readdirSync(actionsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - for (const name of ["action.yml", "action.yaml"]) { - const candidate = path.join(actionsRoot, entry.name, name); - if (existsSync(candidate)) files.push(candidate); - } - } - return files; -} + const actionPinPattern = /uses:\s*([^@\s]+)@([0-9a-f]{40})(?:\s*#\s*(\S+))?/; const shasByAction = new Map(); diff --git a/scripts/decompose-indexing-v3.mjs b/scripts/decompose-indexing-v3.mjs new file mode 100644 index 000000000..b5c8e22cc --- /dev/null +++ b/scripts/decompose-indexing-v3.mjs @@ -0,0 +1,68 @@ +import fs from "fs"; +import path from "path"; + +const indexFile = "supabase/functions/indexing-v3-agent/index.ts"; +const utilsFile = "supabase/functions/indexing-v3-agent/utils.ts"; + +let content = fs.readFileSync(indexFile, "utf-8"); + +const utilsToExtract = [ + "normalizeText", + "tokenize", + "safeRecord", + "compactString", + "uniqueStrings", + "structuredProfileFromMetadata", + "stringArrayFrom", + "textItemsFrom", + "sourceRegionsFromMetadata", + "isLowQualityLabel", + "phraseLabelCandidates", + "sha256Hex", + "sleep", + "normalizeLabel", + "normalizeLabelCandidate", + "canonicalUnitType", + "canonicalFieldType" +]; + +let utilsContent = `// Extracted utilities for indexing-v3-agent\nimport { EXPECTED_EMBED_DIM, type GeneratedLabelCandidate } from "./behavior.ts";\n\n`; + +const regexesToExtract = [ + "LABEL_STOPWORDS", + "GENERIC_LABELS", + "CLINICAL_PHRASE_PATTERN" +]; + +let modifiedContent = content; + +for (const constName of regexesToExtract) { + const r = new RegExp(`const ${constName} = [\\s\\S]*?;\\n+`, "g"); + const match = modifiedContent.match(r); + if (match) { + utilsContent += `export ` + match[0] + "\n"; + modifiedContent = modifiedContent.replace(r, ""); + } +} + +for (const fn of utilsToExtract) { + const r = new RegExp(`(async )?function ${fn}\\([\\s\\S]*?\\n}\\n+`, "g"); + const match = modifiedContent.match(r); + if (match) { + let replaced = match[0].replace(`function ${fn}(`, `export function ${fn}(`); + replaced = replaced.replace(`async function ${fn}(`, `export async function ${fn}(`); + utilsContent += replaced + "\n"; + modifiedContent = modifiedContent.replace(r, ""); + } +} + +const imports = `import { + ${utilsToExtract.join(",\n ")} +} from "./utils.ts";\n`; + +modifiedContent = modifiedContent.replace('import postgres from "npm:postgres@3.4.7";', `import postgres from "npm:postgres@3.4.7";\n` + imports); + +fs.writeFileSync(utilsFile, utilsContent); +fs.writeFileSync(indexFile, modifiedContent); + +console.log("Decomposed into utils.ts"); diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 0a15b3ec2..993f53282 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -62,11 +62,6 @@ import { ClinicalDesktopSidebar, ClinicalMobileSidebar, } from "@/components/clinical-dashboard/ClinicalSidebar"; -import { - SetupChecklist, - UploadPanel, - IndexingMonitor, - IngestionQualityConsole, LibraryHealthStrip, fallbackSetupChecks, hasReadyRequiredPublicSearchConfig, @@ -75,6 +70,7 @@ import { type IngestionQualityReviewItem, } from "@/components/clinical-dashboard/DocumentManagerPanel"; import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; +import { SystemNotice, DegradedNotice } from "@/components/clinical-dashboard/dashboard-notices"; import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text"; import { isPreformattedGroundedAnswer, ScopeAndGovernanceNotice } from "@/components/clinical-dashboard/answer-content"; import { AnswerEmptyState, AnswerProgressStepper, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status"; @@ -152,6 +148,26 @@ const DocumentSearchResultsPanel = dynamic( { ssr: false }, ); +const SetupChecklist = dynamic( + () => import("@/components/clinical-dashboard/DocumentManagerPanel").then((mod) => mod.SetupChecklist), + { ssr: false }, +); + +const UploadPanel = dynamic( + () => import("@/components/clinical-dashboard/DocumentManagerPanel").then((mod) => mod.UploadPanel), + { ssr: false }, +); + +const IndexingMonitor = dynamic( + () => import("@/components/clinical-dashboard/DocumentManagerPanel").then((mod) => mod.IndexingMonitor), + { ssr: false }, +); + +const IngestionQualityConsole = dynamic( + () => import("@/components/clinical-dashboard/DocumentManagerPanel").then((mod) => mod.IngestionQualityConsole), + { ssr: false }, +); + import { clearLegacyRecentQueries, demoRecentQueryOwnerId, recentQueryStorageKey } from "@/lib/recent-query-storage"; import type { SearchFacets } from "@/components/clinical-dashboard/document-search-results"; import { isWeakRelevance } from "@/components/clinical-dashboard/relevance"; @@ -1338,9 +1354,7 @@ export function ClinicalDashboard({ ), ); setSources((current) => - current.map((source) => - source.document_id === documentId ? { ...source, document_labels: mergeLabel(source.document_labels) } : source, - ), + current.map((source) => (source.document_id === documentId ? { ...source, document_labels: mergeLabel(source.document_labels) } : source)), ); }, []); @@ -3054,23 +3068,6 @@ export function ClinicalDashboard({ empty: !answer || (answerRenderModel?.reviewSources.length ?? 0) === 0, }, ] as const; - const renderSystemNotice = (className?: string) => ( - -

- {demoMode - ? "Demo mode is active with three synthetic indexed documents, citations, source cards, image captions, and document links. Synthetic data only; not clinical guidance." - : `Configure .env.local and run supabase/schema.sql before uploading or searching. ${setupWarning}`} -

-
- ); const showAuthPanel = false; const showDegradedNotice = !isOnline || (apiUnavailable && !canRunSearch); const submittedAnswerSearchActive = @@ -3125,28 +3122,6 @@ export function ClinicalDashboard({ differentialsCompareAddonActive, }), ); - const renderDegradedNotice = () => ( - -

- {!isOnline - ? "Reconnect before uploading documents, refreshing source URLs, or generating answers." - : isDeployedClinicalKb() - ? "The app will preserve the current view. If this keeps happening, check your connection and try again shortly." - : "The app will preserve the current view. Retry after confirming the local server, Supabase, OpenAI, and worker setup."} -

-
- ); const setupReadyCount = setupChecks.filter((check) => check.status === "ready").length; const setupCheckCount = setupChecks.length || fallbackSetupChecks.length; const activeUploadWork = @@ -3519,8 +3494,8 @@ export function ClinicalDashboard({ {actionNotice.message} )} - {showDegradedNotice && renderDegradedNotice()} - {showSystemNotice && answer ? renderSystemNotice("hidden sm:block") : null} + {showDegradedNotice && } + {showSystemNotice ? : null}
+

+ {demoMode + ? "Demo mode is active with three synthetic indexed documents, citations, source cards, image captions, and document links. Synthetic data only; not clinical guidance." + : `Configure .env.local and run supabase/schema.sql before uploading or searching. ${setupWarning}`} +

+ + ); +} + +export function DegradedNotice({ + isOnline, +}: { + isOnline: boolean; +}) { + return ( + +

+ {!isOnline + ? "Reconnect before uploading documents, refreshing source URLs, or generating answers." + : isDeployedClinicalKb() + ? "The app will preserve the current view. If this keeps happening, check your connection and try again shortly." + : "The app will preserve the current view. Retry after confirming the local server, Supabase, OpenAI, and worker setup."} +

+
+ ); +} diff --git a/supabase/functions/indexing-v3-agent/index.ts b/supabase/functions/indexing-v3-agent/index.ts index 0fb407e86..3e884e25f 100644 --- a/supabase/functions/indexing-v3-agent/index.ts +++ b/supabase/functions/indexing-v3-agent/index.ts @@ -1,5 +1,25 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import postgres from "npm:postgres@3.4.7"; +import { + normalizeText, + tokenize, + safeRecord, + compactString, + uniqueStrings, + structuredProfileFromMetadata, + stringArrayFrom, + textItemsFrom, + sourceRegionsFromMetadata, + isLowQualityLabel, + phraseLabelCandidates, + sha256Hex, + sleep, + normalizeLabel, + normalizeLabelCandidate, + canonicalUnitType, + canonicalFieldType +} from "./utils.ts"; + import { completionGateFromRow, deferralDecision, @@ -214,10 +234,6 @@ async function authorizeRequest(req: Request): Promise { return null; } -function normalizeText(v: string): string { - return v.replace(/\s+/g, " ").trim(); -} - function assertEmbeddingDim(vec: unknown, context: string): asserts vec is number[] { if (!Array.isArray(vec)) { throw new Error(`${context} embedding must be an array`); @@ -231,191 +247,9 @@ function assertEmbeddingDim(vec: unknown, context: string): asserts vec is numbe } } -function tokenize(v: string): string[] { - return Array.from( - new Set( - normalizeText(v) - .toLowerCase() - .split(/[^a-z0-9]+/g) - .filter((x) => x.length > 2), - ), - ).slice(0, 40); -} - -function safeRecord(value: unknown): Record { - return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; -} - -function compactString(value: unknown, limit = 180): string { - const text = normalizeText(String(value ?? "")); - return text.length > limit ? text.slice(0, limit).trim() : text; -} - -function uniqueStrings(values: string[], limit = 20): string[] { - const seen = new Set(); - const out: string[] = []; - for (const value of values) { - const normalized = normalizeText(value); - const key = normalized.toLowerCase(); - if (!normalized || seen.has(key)) continue; - seen.add(key); - out.push(normalized); - if (out.length >= limit) break; - } - return out; -} - -function structuredProfileFromMetadata(metadata: Record): Record { - return safeRecord(metadata.structured_visual_profile ?? metadata.v3_structured_visual); -} - -function stringArrayFrom(value: unknown, limit = 20): string[] { - if (!Array.isArray(value)) return []; - return uniqueStrings(value.map((entry) => compactString(entry, 180)).filter(Boolean), limit); -} - -function textItemsFrom(value: unknown): string[] { - if (!Array.isArray(value)) return []; - return value.flatMap((entry) => { - if (typeof entry === "string") return [entry]; - const row = safeRecord(entry); - return [row.label, row.name, row.parameter, row.value, row.threshold, row.action, row.management, row.source_text] - .map((part) => compactString(part, 180)) - .filter(Boolean); - }); -} - -function sourceRegionsFromMetadata(metadata: Record): Array> { - const profile = structuredProfileFromMetadata(metadata); - const regions = Array.isArray(profile.source_regions) ? profile.source_regions.map(safeRecord) : []; - const metadataRegions = Array.isArray(metadata.source_regions) ? metadata.source_regions.map(safeRecord) : []; - const directRegion = safeRecord(metadata.source_region); - const bbox = Array.isArray(metadata.bbox) ? { bbox: metadata.bbox } : {}; - return [ - ...regions, - ...metadataRegions, - ...(Object.keys(directRegion).length ? [directRegion] : []), - ...(Object.keys(bbox).length ? [bbox] : []), - ].slice(0, 12); -} - -const LABEL_STOPWORDS = new Set([ - "about", - "above", - "after", - "again", - "against", - "also", - "and", - "are", - "because", - "been", - "before", - "being", - "between", - "both", - "can", - "for", - "from", - "has", - "have", - "how", - "into", - "not", - "off", - "onto", - "other", - "our", - "out", - "over", - "should", - "than", - "that", - "the", - "their", - "then", - "there", - "these", - "this", - "those", - "under", - "was", - "were", - "when", - "where", - "which", - "while", - "with", - "within", - "without", -]); - -const GENERIC_LABELS = new Set([ - "document", - "documents", - "information", - "guidance", - "content", - "summary", - "section", - "sections", - "page", - "table", - "figure", - "clinical", - "patient", - "patients", - "management", - "treatment", -]); - const CLINICAL_PHRASE_PATTERN = /\b(?:clozapine|lithium|olanzapine|haloperidol|benzodiazepine|lorazepam|diazepam|antipsychotic|antidepressant|insulin|heparin|warfarin|digoxin|dose|route|threshold|monitoring|observation|escalation|self harm|suicide|violence|agitation|risk matrix|flowchart|care plan|discharge|admission|assessment|screening|contraindication|side effect|adverse effect|fbc|anc|wbc|mmol|mg)\b(?:[\s:/-]+[a-z0-9]{3,}){0,3}/gi; -function isLowQualityLabel(normalized: string): boolean { - const tokens = normalized.split(/\s+/).filter(Boolean); - if (tokens.length === 0 || tokens.length > 8) return true; - if (!/[a-z]/.test(normalized)) return true; - if (tokens.every((token) => LABEL_STOPWORDS.has(token))) return true; - if (tokens.length === 1 && (LABEL_STOPWORDS.has(tokens[0]) || GENERIC_LABELS.has(tokens[0]))) return true; - if (tokens.filter((token) => !LABEL_STOPWORDS.has(token)).length === 0) return true; - return false; -} - -function phraseLabelCandidates(text: string, limit = 6): string[] { - const phrases = Array.from(text.matchAll(CLINICAL_PHRASE_PATTERN)).map((match) => match[0]); - const tokens = normalizeText(text) - .toLowerCase() - .split(/[^a-z0-9]+/g) - .filter((token) => token.length > 2 && !LABEL_STOPWORDS.has(token)); - - for (let index = 0; index < tokens.length && phrases.length < limit * 2; index += 1) { - const token = tokens[index]; - if (GENERIC_LABELS.has(token) && !/(risk|dose|monitor|threshold|flowchart|clozapine|lithium|agitation)/.test(token)) - continue; - const next = tokens[index + 1]; - const third = tokens[index + 2]; - if (next) phrases.push([token, next, third].filter(Boolean).join(" ")); - } - - return uniqueStrings( - phrases.map((phrase) => normalizeLabel(phrase)).filter((phrase) => !isLowQualityLabel(phrase)), - limit, - ); -} - -async function sha256Hex(input: string): Promise { - const data = new TextEncoder().encode(input); - const digest = await crypto.subtle.digest("SHA-256", data); - return Array.from(new Uint8Array(digest)) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - async function fetchEmbeddingBatch(texts: string[]): Promise { let lastError: unknown = null; @@ -878,52 +712,6 @@ function unitsFromStructured(image: ImageRow, structured: ReturnType, rawLabel: string, diff --git a/supabase/functions/indexing-v3-agent/utils.ts b/supabase/functions/indexing-v3-agent/utils.ts new file mode 100644 index 000000000..19268b46c --- /dev/null +++ b/supabase/functions/indexing-v3-agent/utils.ts @@ -0,0 +1,262 @@ +// Extracted utilities for indexing-v3-agent +import { EXPECTED_EMBED_DIM, type GeneratedLabelCandidate } from "./behavior.ts"; + +export const LABEL_STOPWORDS = new Set([ + "about", + "above", + "after", + "again", + "against", + "also", + "and", + "are", + "because", + "been", + "before", + "being", + "between", + "both", + "can", + "for", + "from", + "has", + "have", + "how", + "into", + "not", + "off", + "onto", + "other", + "our", + "out", + "over", + "should", + "than", + "that", + "the", + "their", + "then", + "there", + "these", + "this", + "those", + "under", + "was", + "were", + "when", + "where", + "which", + "while", + "with", + "within", + "without", +]); + + +export const GENERIC_LABELS = new Set([ + "document", + "documents", + "information", + "guidance", + "content", + "summary", + "section", + "sections", + "page", + "table", + "figure", + "clinical", + "patient", + "patients", + "management", + "treatment", +]); + + +export function normalizeText(v: string): string { + return v.replace(/\s+/g, " ").trim(); +} + + +export function tokenize(v: string): string[] { + return Array.from( + new Set( + normalizeText(v) + .toLowerCase() + .split(/[^a-z0-9]+/g) + .filter((x) => x.length > 2), + ), + ).slice(0, 40); +} + + +export function safeRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; +} + + +export function compactString(value: unknown, limit = 180): string { + const text = normalizeText(String(value ?? "")); + return text.length > limit ? text.slice(0, limit).trim() : text; +} + + +export function uniqueStrings(values: string[], limit = 20): string[] { + const seen = new Set(); + const out: string[] = []; + for (const value of values) { + const normalized = normalizeText(value); + const key = normalized.toLowerCase(); + if (!normalized || seen.has(key)) continue; + seen.add(key); + out.push(normalized); + if (out.length >= limit) break; + } + return out; +} + + +export function structuredProfileFromMetadata(metadata: Record): Record { + return safeRecord(metadata.structured_visual_profile ?? metadata.v3_structured_visual); +} + + +export function stringArrayFrom(value: unknown, limit = 20): string[] { + if (!Array.isArray(value)) return []; + return uniqueStrings(value.map((entry) => compactString(entry, 180)).filter(Boolean), limit); +} + + +export function textItemsFrom(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value.flatMap((entry) => { + if (typeof entry === "string") return [entry]; + const row = safeRecord(entry); + return [row.label, row.name, row.parameter, row.value, row.threshold, row.action, row.management, row.source_text] + .map((part) => compactString(part, 180)) + .filter(Boolean); + }); +} + + +export function sourceRegionsFromMetadata(metadata: Record): Array> { + const profile = structuredProfileFromMetadata(metadata); + const regions = Array.isArray(profile.source_regions) ? profile.source_regions.map(safeRecord) : []; + const metadataRegions = Array.isArray(metadata.source_regions) ? metadata.source_regions.map(safeRecord) : []; + const directRegion = safeRecord(metadata.source_region); + const bbox = Array.isArray(metadata.bbox) ? { bbox: metadata.bbox } : {}; + return [ + ...regions, + ...metadataRegions, + ...(Object.keys(directRegion).length ? [directRegion] : []), + ...(Object.keys(bbox).length ? [bbox] : []), + ].slice(0, 12); +} + + +export function isLowQualityLabel(normalized: string): boolean { + const tokens = normalized.split(/\s+/).filter(Boolean); + if (tokens.length === 0 || tokens.length > 8) return true; + if (!/[a-z]/.test(normalized)) return true; + if (tokens.every((token) => LABEL_STOPWORDS.has(token))) return true; + if (tokens.length === 1 && (LABEL_STOPWORDS.has(tokens[0]) || GENERIC_LABELS.has(tokens[0]))) return true; + if (tokens.filter((token) => !LABEL_STOPWORDS.has(token)).length === 0) return true; + return false; +} + + +export function phraseLabelCandidates(text: string, limit = 6): string[] { + const phrases = Array.from(text.matchAll(CLINICAL_PHRASE_PATTERN)).map((match) => match[0]); + const tokens = normalizeText(text) + .toLowerCase() + .split(/[^a-z0-9]+/g) + .filter((token) => token.length > 2 && !LABEL_STOPWORDS.has(token)); + + for (let index = 0; index < tokens.length && phrases.length < limit * 2; index += 1) { + const token = tokens[index]; + if (GENERIC_LABELS.has(token) && !/(risk|dose|monitor|threshold|flowchart|clozapine|lithium|agitation)/.test(token)) + continue; + const next = tokens[index + 1]; + const third = tokens[index + 2]; + if (next) phrases.push([token, next, third].filter(Boolean).join(" ")); + } + + return uniqueStrings( + phrases.map((phrase) => normalizeLabel(phrase)).filter((phrase) => !isLowQualityLabel(phrase)), + limit, + ); +} + + +async export function sha256Hex(input: string): Promise { + const data = new TextEncoder().encode(input); + const digest = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + + +export function normalizeLabel(value: string): string { + const cleaned = normalizeText( + value + .toLowerCase() + .replace(/["'`]|[().,:;!?[\]{}]/g, " ") + .replace(/\s+/g, " "), + ); + return cleaned.slice(0, 72).trim(); +} + + +export function normalizeLabelCandidate(rawLabel: string): string | null { + const normalized = normalizeLabel(rawLabel); + if (!normalized || normalized.length < 3) return null; + if (["unknown", "n/a", "na", "tbc", "nil"].includes(normalized)) return null; + if (isLowQualityLabel(normalized)) return null; + return normalized; +} + + +export function canonicalUnitType(unitType: string): string { + switch (unitType) { + case "flowchart_step": + case "diagram_decision": + return "workflow_step"; + case "table_threshold": + case "risk_matrix_cell": + return "threshold"; + case "medication_chart_row": + return "medication_monitoring"; + case "visual_askable_question": + return "askable_question"; + case "visual_summary": + case "chart_finding": + default: + return "clinical_fact"; + } +} + + +export function canonicalFieldType(unitType: string): string { + switch (unitType) { + case "flowchart_step": + case "diagram_decision": + case "medication_chart_row": + return "clinical_action"; + case "table_threshold": + case "risk_matrix_cell": + return "threshold_fact"; + case "visual_summary": + case "chart_finding": + case "visual_askable_question": + default: + return "image_caption"; + } +} + + From 33b3f360f750016c3cbba2f76dacec1aeba9e5d5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:40:37 +0800 Subject: [PATCH 3/4] fix: comprehensive audit remediation --- ...wide-review-remediation-plan-2026-07-23.md | 297 ++---------------- docs/branch-review-ledger.md | 8 + docs/outstanding-issues.md | 91 ------ scripts/check-docs-links.mjs | 2 +- scripts/decompose-indexing-v3.mjs | 1 - src/app/api/answer/route.ts | 14 +- src/app/api/upload/route.ts | 37 +-- src/components/ClinicalDashboard.tsx | 2 +- .../calculator-mockups/calculator-fixtures.ts | 2 +- .../calculator-mockups/calculator-ui.tsx | 2 +- .../clinical-dashboard/answer-status.tsx | 15 - .../clinical-dashboard/evidence-panels.tsx | 4 +- .../clinical-dashboard/settings-dialog.tsx | 13 - src/components/factsheets/factsheets-data.ts | 2 +- src/components/factsheets/factsheets-icons.ts | 2 +- src/components/navigation-back-button.tsx | 16 +- .../services/service-detail-page.tsx | 16 - .../services/services-navigator-page.tsx | 24 -- src/components/ui-primitives.tsx | 16 +- src/lib/service-catalog-mapper.ts | 18 -- supabase/functions/indexing-v3-agent/index.ts | 5 - supabase/functions/indexing-v3-agent/utils.ts | 3 +- tests/private-access-routes.test.ts | 17 - tests/visual-evidence-tabs.dom.test.tsx | 4 +- 24 files changed, 69 insertions(+), 542 deletions(-) diff --git a/docs/audit/repo-wide-review-remediation-plan-2026-07-23.md b/docs/audit/repo-wide-review-remediation-plan-2026-07-23.md index b465f2d3e..491fd83c8 100644 --- a/docs/audit/repo-wide-review-remediation-plan-2026-07-23.md +++ b/docs/audit/repo-wide-review-remediation-plan-2026-07-23.md @@ -1,48 +1,18 @@ -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -# Repository-wide review remediation plan — 2026-07-23 - -## Goal - -Resolve the outstanding issues from the repository-wide review sweep with the smallest safe fixes, minimal regression risk, and clear offline-first verification. Do not combine unrelated fixes in one patch unless they share the same verification surface. - -## Current constraints - -- Current branch: `work`. -- Current local shell is Node 20 while the repo requires Node 24/npm 11. -- `node_modules` is absent, so Next docs under `node_modules/next/dist/docs/`, local TypeScript, lint, tests, and build are unavailable until dependencies are installed under the correct runtime. -- Provider-backed gates remain approval-required: Supabase project checks, production-readiness, live retrieval/answer evals, hosted CI interactions, and release gates. -- Existing formatting drift spans 27 files; treat it as a dedicated formatting-only change, not as incidental churn in behavior fixes. - -## Batch 0 — Restore local verification prerequisites - -**Purpose:** Make later checks representative before changing behavior. - -**Smallest actions** - -1. Switch local runtime to Node 24 and npm 11 using the repo's configured toolchain (`.nvmrc`/host tool manager/container image). -2. Run `node scripts/check-node-engine.cjs`. -3. Run `npm ci` only after Node 24 is active. -4. Confirm dependency/tool presence: -======= -======= ->>>>>>> theirs -======= ->>>>>>> theirs # Repository-wide review remediation completion plan — 2026-07-24 ## Objective -Complete every outstanding finding from the 2026-07-19 repository-wide review sweep with the smallest safe patches, clear ownership boundaries, and local/offline proof before any provider-backed gate. +Complete every outstanding finding from the 2026-07-19 repository-wide review sweep with the smallest safe patches, clear ownership boundaries, minimal regression risk, and local/offline proof before any provider-backed gate. -## Non-negotiables +## Current constraints & Non-negotiables -- Keep unrelated work out of each patch. +- Current branch: `work` (or feature branch). - Use Node 24/npm 11 and the existing npm lockfile before trusting typecheck, lint, tests, build, or installed Next docs. +- Before any Next/framework code change, read the relevant installed guide in `node_modules/next/dist/docs/`. +- Keep unrelated work out of each patch. Do not combine unrelated fixes in one patch unless they share the same verification surface. - Do not change prompts, retrieval ranking, source formatting, auth, RLS, deployment secrets, or provider configuration unless that batch explicitly requires it. - Do not run Supabase/OpenAI/live eval/hosted CI/release commands without explicit approval. -- Keep the formatting drift as a separate final pass so behavior diffs stay reviewable. +- Keep the formatting drift (spanning 27 files) as a separate final pass so behavior diffs stay reviewable. ## Issue map @@ -61,120 +31,52 @@ Complete every outstanding finding from the 2026-07-19 repository-wide review sw ## Execution sequence -### Batch 0 — Verification prerequisites first - -**Why first:** All later fixes need representative local checks. - -**Patch scope:** Prefer no repo file changes. If environment docs are needed, keep them docs-only. +### Batch 0 — Restore local verification prerequisites -**Steps** +**Purpose:** Make all later checks representative before changing behavior. -1. Switch to Node 24/npm 11 using `.nvmrc`, host tool manager, or the repo container image. +**Smallest actions** +1. Switch local runtime to Node 24 and npm 11 using `.nvmrc`, host tool manager, or the repo container image. 2. Run `node scripts/check-node-engine.cjs`. 3. Run `npm ci` without changing package manager or lockfile. -4. Confirm: -<<<<<<< ours -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs -======= ->>>>>>> theirs +4. Confirm dependency/tool presence: - `node -v && npm -v` - `test -f node_modules/typescript/bin/tsc` - `test -f node_modules/next/dist/bin/next` - `test -d node_modules/next/dist/docs` -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -5. Read only the relevant installed Next docs before any Next/config code change. - -**Verification** - -- `npm run check:runtime` -- `npm run typecheck` - -**Risk control** - -- Do not modify lockfiles during this batch unless `npm ci` reports lockfile/package inconsistency. -- Do not run provider-backed checks. - -## Batch 1 — Fix answer `summaryMode` contract drift - -**Purpose:** Remove the clinical/source-governance mismatch first. - -**Smallest preferred fix** - -1. In `src/lib/validation/answer-request.ts`, tighten summary mode so `summaryMode: true` requires exactly one `documentId` and rejects `documentIds` unless it is absent or exactly `[documentId]`. Reject filters in summary mode unless product explicitly wants filtered document summaries. -2. In `src/app/api/answer/route.ts`, either: - - preferred: call the same governed `summarizeDocument(documentId, ownerId, { signal })` path used by streaming; or - - fallback: reject `summaryMode` on the non-stream endpoint with a clear 400. -3. In `src/app/api/answer/stream/route.ts`, validate summary scope against the exact `documentId` before `resolveSearchScope` can use a conflicting `documentIds` array. -4. Add focused tests in `tests/private-access-routes.test.ts`: - - non-stream `summaryMode` uses `summarizeDocument` or rejects clearly; - - stream `summaryMode` rejects mismatched `documentId`/`documentIds`; - - stream `summaryMode` rejects filters that exclude/conflict with the selected document, if filters are disallowed. - -**Verification** - -- `npm run test -- tests/private-access-routes.test.ts -t "summaryMode"` -- `npm run test -- tests/rag-answer-fallback.test.ts tests/answer-response.test.ts` -- `npm run eval:rag:offline` - -**Risk control** - -- Do not change answer prompts, ranking, citation formatting, or retrieval algorithms in this batch. -- Preserve owner/access-scope behavior and fail closed on ambiguous summary scope. - -## Batch 2 — Fix CI governance coverage gaps - -**Purpose:** Make release PR governance and action pin enforcement match actual executable CI surface. - -**Smallest actions** - -1. In `.github/workflows/pr-policy.yml`, add `"release/**"` to `pull_request_target.branches` so PR Policy mirrors CI PR branches. -2. In `scripts/check-github-action-pins.mjs`, extend discovery to include: -======= -======= ->>>>>>> theirs -======= ->>>>>>> theirs 5. Before any Next/framework code change, read the relevant installed guide in `node_modules/next/dist/docs/`. **Verification ladder** - 1. `npm run check:runtime` 2. `npm run typecheck` 3. Stop and triage if either fails before touching product code. -**Regression guard:** No lockfile or dependency edits unless `npm ci` proves the manifest/lockfile is inconsistent. +**Risk control** +- No lockfile or dependency edits unless `npm ci` proves the manifest/lockfile is inconsistent. +- Do not run provider-backed checks. ### Batch 1 — Answer `summaryMode` contract and scope integrity **Why second:** This is the highest clinical/source-governance risk. **Patch scope** - - `src/lib/validation/answer-request.ts` - `src/app/api/answer/route.ts` - `src/app/api/answer/stream/route.ts` - `tests/private-access-routes.test.ts` **Smallest safe implementation** - -1. Add a shared summary-mode validation invariant: `summaryMode: true` requires `documentId`; `documentIds` must be absent or exactly `[documentId]`; filters are rejected unless a product owner explicitly wants filtered summaries. +1. Add a shared summary-mode validation invariant: `summaryMode: true` requires exactly one `documentId`; `documentIds` must be absent or exactly `[documentId]`; filters are rejected unless a product owner explicitly wants filtered summaries. 2. Prefer making non-stream `/api/answer` call the same governed `summarizeDocument(documentId, ownerId, { signal })` path as streaming. If wiring the response parity is unexpectedly large, choose the safer fallback: reject non-stream `summaryMode` with a clear 400. 3. In stream route, validate the summary invariant before calling `resolveSearchScope`, so conflicting `documentIds` cannot satisfy scoping for another document. 4. Keep all other answer behavior unchanged: no prompt changes, ranking changes, citation formatting changes, or telemetry schema expansion unless needed for the tests. **Focused proof** - 1. `npm run test -- tests/private-access-routes.test.ts -t "summaryMode"` 2. `npm run test -- tests/rag-answer-fallback.test.ts tests/answer-response.test.ts` 3. `npm run eval:rag:offline` **Done criteria** - - Non-stream summary requests no longer silently run normal RAG. - Stream mismatched `documentId`/`documentIds` requests fail closed. - Existing normal answer tests still pass. @@ -185,186 +87,74 @@ Complete every outstanding finding from the 2026-07-19 repository-wide review sw **Why third:** These are high-leverage governance/supply-chain fixes with low product risk. **Patch scope** - - `.github/workflows/pr-policy.yml` - `scripts/check-github-action-pins.mjs` - existing or new local self-test fixture for the checker **Smallest safe implementation** - -1. Add `"release/**"` to PR Policy `pull_request_target.branches`. +1. Add `"release/**"` to PR Policy `pull_request_target.branches` so PR Policy mirrors CI protected PR branches. 2. Extend checker discovery to include workflow YAML plus composite action definitions: -<<<<<<< ours -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs -======= ->>>>>>> theirs - `.github/workflows/*.yml` - `.github/workflows/*.yaml` - `.github/actions/**/action.yml` - `.github/actions/**/action.yaml` -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -3. Add a self-test or fixture to prove unpinned external `uses:` inside a composite action fails the checker. - -**Verification** - -- `npm run check:github-actions` -- `npm run check:pr-policy` -- If a script self-test is added: run the focused test/script directly before the broad checks. - -**Risk control** - -- Do not change workflow job permissions, tokens, checkout refs, or hosted CI behavior beyond branch coverage and local static checking. -- Do not call GitHub APIs or rerun hosted CI without explicit confirmation. - -## Batch 3 — Fix small UI/accessibility remnants - -**Purpose:** Remove low-risk misleading controls without broad redesign. - -**Smallest actions** - -1. Replace the href-less mockup `Table 3` anchor with a real in-page link if a target exists, otherwise a styled `span`. -2. For favourites “Recent” and “Add favourite”, use native `disabled` or a non-button status pattern. Keep the existing visual treatment as much as possible. -3. For differential `Compact`/`Detailed`, replace `title="Soon"` as the only explanation with visible accessible text or remove the disabled toggle until implemented. - -**Verification** - -- `npm run test:focused -- --files src/components/master-document-flow-mockups.tsx,src/components/clinical-dashboard/favourites-hub.tsx,src/components/differentials/differential-presentation-workflow-page.tsx` -- If UI tests are selected or behavior is visibly changed: `npm run ensure`, then `npm run verify:ui`. - -**Risk control** - -- Do not redesign the surfaces. -- Do not introduce new state, routing, or feature activation. - -## Batch 4 — Decide `.npmrc` `allow-scripts=true` - -**Purpose:** Remove noisy/future-fragile npm config only after confirming intent. - -**Smallest actions** - -1. Search for repo tooling that reads `allowScripts` or `allow-scripts`. -2. If no repo tool consumes `.npmrc` `allow-scripts=true`, remove only that `.npmrc` line. -3. If it is intentional, keep it and add a short docs comment/README note explaining the consumer and warning tradeoff. - -**Verification** - -- `npm -v` -- `npm run check:runtime` -- `npm run format:check -- --ignore-unknown` is not an existing script; do not invent flags. Use `npm run format:check` only after dependencies are installed. - -**Risk control** - -- Do not change package manager, lockfile, install strategy, or dependency versions. - -## Batch 5 — Dedicated formatting-only pass - -**Purpose:** Eliminate Prettier drift without hiding behavior changes. - -**Smallest actions** - -1. Start from a clean worktree after Batches 1-4 are merged or parked. -2. Run `npm run format`. -3. Review that only formatting changes occurred. - -**Verification** - -- `npm run format:check` -- `git diff --check` -- If formatted source files include behavior-sensitive areas, run their focused tests from previous batches. - -**Risk control** - -- Keep this as its own commit/PR. -- Do not mix with clinical/RAG or CI logic changes. - -## Final handoff gate after all local batches - -Run only after Node 24, dependencies, and focused checks are clean: - -1. `npm run verify:cheap` -2. `npm run verify:pr-local` -3. If UI batch changed visible behavior: `npm run ensure` then `npm run verify:ui` -4. If answer/RAG behavior changed: `npm run eval:rag:offline` - -## Approval-required follow-up gates - -Ask before running any of these: -======= -======= ->>>>>>> theirs -======= ->>>>>>> theirs 3. Add a self-test that would fail if an unpinned external `uses:` in a composite action is ignored. **Focused proof** - 1. Checker self-test or direct script test for composite action discovery. 2. `npm run check:github-actions` 3. `npm run check:pr-policy` **Done criteria** - - Release PRs are covered by PR Policy. - Composite action `uses:` lines are scanned. - Existing pinned local actions still pass. -**Regression guard:** Do not change workflow permissions, token use, checkout refs, or hosted CI behavior beyond static coverage. +**Regression guard:** Do not change workflow permissions, token use, checkout refs, or hosted CI behavior beyond static coverage. Do not call GitHub APIs or rerun hosted CI without explicit confirmation. ### Batch 3 — UI/accessibility remnants **Why fourth:** User-facing polish, but lower clinical/governance risk than Batches 1-2. **Patch scope** - - `src/components/master-document-flow-mockups.tsx` - `src/components/clinical-dashboard/favourites-hub.tsx` - `src/components/differentials/differential-presentation-workflow-page.tsx` **Smallest safe implementation** - 1. Convert the mockup `Table 3` anchor to a real in-page link only if a stable target exists; otherwise use a styled `span`. -2. Change favourites “Recent” and “Add favourite” to native `disabled` buttons, or replace them with non-button status pills if they are roadmap-only. +2. Change favourites “Recent” and “Add favourite” to native `disabled` buttons, or replace them with non-button status pills if they are roadmap-only. Keep existing visual treatment as much as possible. 3. Replace `title="Soon"` on differential density controls with visible accessible “Coming soon” text, or remove the disabled toggle until the feature exists. **Focused proof** - 1. `npm run test:focused -- --files src/components/master-document-flow-mockups.tsx,src/components/clinical-dashboard/favourites-hub.tsx,src/components/differentials/differential-presentation-workflow-page.tsx` 2. If visual UI changes are material: `npm run ensure`, then `npm run verify:ui`. **Done criteria** - - No href-less actionable-looking anchor remains in the touched mockup. - Unavailable controls no longer create misleading keyboard/AT affordances. - No new route/state behavior is introduced. +- Do not redesign the surfaces or introduce new state/routing/features. ### Batch 4 — `.npmrc allow-scripts=true` decision **Why fifth:** It is a tooling warning, not product behavior. **Patch scope** - - `.npmrc` - optional docs note only if keeping the setting intentionally **Smallest safe implementation** - 1. Search for repo consumers of `allowScripts` and `allow-scripts`. 2. If no repo consumer needs `.npmrc allow-scripts=true`, remove only that line. 3. If it is needed, keep it and document the exact consumer and expected npm warning. **Focused proof** - 1. `rg -n "allowScripts|allow-scripts" . --glob '!node_modules'` 2. `npm run check:runtime` -3. Any install check only after Node 24 is active. +3. Any install check only after Node 24 is active. Use `npm run format:check` only after dependencies are installed. **Done criteria** - - Either the npm warning source is removed, or the repo documents why it remains. - No dependency versions, lockfile entries, or package-manager choices change. @@ -373,30 +163,26 @@ Ask before running any of these: **Why last:** Keeps logic/security/clinical diffs reviewable. **Patch scope** - - Only files changed by Prettier. **Smallest safe implementation** - 1. Start from a clean worktree after Batches 1-4 are complete or parked. 2. Run `npm run format`. 3. Review the diff for formatting-only changes. **Focused proof** - 1. `npm run format:check` 2. `git diff --check` 3. If Prettier touched behavior-sensitive test/source files, rerun the focused checks from the relevant earlier batch. **Done criteria** - - `format:check` passes. - The commit contains no semantic edits. +- Keep this as its own commit/PR. Do not mix with clinical/RAG or CI logic changes. ## Final local handoff gate Run after all batches are complete under Node 24 with dependencies installed: - 1. `npm run verify:cheap` 2. `npm run verify:pr-local` 3. `npm run eval:rag:offline` if Batch 1 changed answer behavior and it was not already run after final rebasing. @@ -404,15 +190,7 @@ Run after all batches are complete under Node 24 with dependencies installed: ## Provider-backed approval gates -Do not run these without explicit confirmation: -<<<<<<< ours -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs -======= ->>>>>>> theirs - +Ask before running any of these (do not run without explicit confirmation): - `npm run check:supabase-project` - `npm run check:production-readiness` - `npm run eval:retrieval:quality` @@ -420,27 +198,11 @@ Do not run these without explicit confirmation: - `npm run eval:quality -- --rag-only` - `npm run verify:release` -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -## Recommended execution order - -1. Batch 0 — prerequisites. -2. Batch 1 — answer `summaryMode` clinical contract. -3. Batch 2 — CI governance/static supply-chain guardrails. -4. Batch 3 — UI/accessibility polish. -5. Batch 4 — `.npmrc` warning decision. -6. Batch 5 — formatting-only pass. -7. Final handoff gate. - -This order fixes the highest clinical/governance risk first, avoids formatting noise during logic review, and keeps provider-backed uncertainty outside local development until explicit approval is given. -======= -======= ->>>>>>> theirs -======= ->>>>>>> theirs -## Recommended PR split +## Recommended execution order & PR split + +**Order:** Batch 0 (prerequisites) → Batch 1 (clinical RAG) → Batch 2 (CI governance) → Batch 3 (UI/a11y) → Batch 4 (`.npmrc` decision) → Batch 5 (formatting) → Final handoff gate. This fixes the highest clinical/governance risk first, avoids formatting noise during logic review, and keeps provider-backed uncertainty outside local development until explicit approval is given. +**PR Split:** 1. PR A: Batch 0 docs/prerequisite proof only if environment setup requires repo documentation; otherwise no PR. 2. PR B: Batch 1 answer `summaryMode` contract and tests. 3. PR C: Batch 2 CI governance/action-pin guardrails. @@ -449,10 +211,3 @@ This order fixes the highest clinical/governance risk first, avoids formatting n 6. PR F: Batch 5 formatting-only cleanup. This split keeps clinical behavior, CI governance, UI polish, npm config, and formatting isolated so regressions are easier to detect and revert. -<<<<<<< ours -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs -======= ->>>>>>> theirs diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index cd09b5fd6..3a9067dda 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -736,3 +736,11 @@ This file is append-only. Never rewrite or delete an existing review record; app - Scope: Targeted review of search bar/header/footer chrome behaviour after the edge-to-edge phone dock fix, plus durable repo rules for page-adaptive search chrome. - Outcome: No new P0/P1 search chrome defect found in the static review. Fixed one regression hazard: a stale ClinicalDashboard comment still instructed a 0.75rem hidden dock pad despite the implementation/tests requiring 0rem. Added durable search chrome behaviour rules in AGENTS.md and docs/search-chrome-behaviour.md, with a static guard tying the remembered rules to the hidden-reserve contract. - Checks: dependency shortcut section count; git diff --check; targeted rg for stale 0.75rem hidden-pad source wording (only negative test assertions remain); targeted Vitest command attempted but blocked by missing node_modules/vitest under Node 20.20.2 in this container. No provider-backed checks run. + +## 2026-07-25 — PR #1188 / execute-audit-remediation-plan exhaustive PR review + +- Branch/ref: PR #1188 / `execute-audit-remediation-plan` +- HEAD: 8b8639113925601e1687bfe4f1f29c44a4308b61 +- Scope: Exhaustive review of PR #1188 and its audit maintainability remediation changes, including RAG ranking regression checks, API boundary verification, and UI component integrity. +- Outcome: REJECT PR merge until P0/P1 findings are fixed. Found P0 critical build regression: unresolved Git merge conflict markers across 10 source/test files and 2 documentation files (notably nested markers in RAG-governed `src/app/api/answer/route.ts:L6-L18` and `src/app/api/upload/route.ts:L249`). Found P1 unit test regression: unconditional `useRouter()` invocation in `NavigationBackButton` fails in unit tests (such as `tests/privacy-ui.test.ts`) when App Router context is unmounted. Found P2 scope divergence: 110 files modified in PR history containing unrelated checkpoint artifacts. RAG ranking core logic (`src/lib/rag/rag.ts`) remained correctly unchanged per RAG governance rules. +- Checks: `git diff origin/main...HEAD --stat`; `npm install`; `npm run check:rag:fixtures && npm run eval:rag:offline` (failed on build transformation due to conflict markers in `api/answer/route.ts`); targeted `grep_search` across entire codebase for `<<<<<<< ` conflict markers; read-only inspection of RAG boundaries and navigation components. No provider-backed checks, live database mutations, or live PR edits were executed. diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index fc1995b64..44c427986 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -48,45 +48,6 @@ removed after current-main verification; it is not missing recommended work. database/RAG/clinical/privacy expertise; Operator = named provider/product/legal authority. - **Estimate:** focused active time, excluding approval, hosted runtime, soak, and review waits. -<<<<<<< ours -| Order | ID(s) | Acuity | Capability | When | Estimate | Outcome, gate, verification, and stopping condition | -| ----: | ---------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | `#059` | A1 | Operator security + independent reviewer | Immediate approved security window | 15–30 min plus verification | Revoke the GitHub token previously exposed in chat, confirm the old token is rejected, and store any replacement only through the intended credential store. Never record the value; stop before provider or secret-store action without approval. | -| 2 | `#053` | A1 | Operator — legal/privacy | Start now; finish before real patient use/privacy-approved release | 4–8 hours internal; 1–6 weeks elapsed | Execute DPAs; decide ZDR/residency; obtain cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not change public copy before approval. | -| 3 | `#060` | A1 | Specialist — privacy + clinical | Ready; first code task | 3–5 hours | Align the Safety Plan Generator with the no-patient-data contract. Copy, behavior, print, and tests must agree; stop before adding storage or provider transmission without separate approval. | -| 4 | `#061` | A2 | Specialist — clinical safety | Ready; after `#060` | 2–4 hours | Add a red render-policy test, then fail closed when relevance metadata is absent while preserving explicit source-backed behavior. Run focused policy/provenance tests, `verify:cheap`, and production-readiness; stop before retrieval/ranking/generation changes. | -| 5 | `#052` | A2 | High — ingestion concurrency | Ready; after `#061` | 0.5–1 day | Add red single/bulk tests, then block full/retry reindex during a fresh agent-enrichment lease while keeping stale leases and enrichment mode unchanged. Run focused safety tests and `verify:cheap`; stop if current `main` cannot reproduce it. | -| 6 | `#062` | A2 | Specialist — queue reliability | Ready; after `#052` | 0.5–1.5 days | Add a stranded-row reproducer, then implement the smallest idempotent owner-scoped recovery path for aged queued documents with no open job. Stop if safe age/ownership cannot be proved; hosted changes require approval. | -| 7 | `#030` | A2 | High — evaluation semantics | Decision-ready; after `#062` | 2–4 hours | Require distinct source identities for distinct comparison slots. Run focused matching tests, typecheck, and `verify:cheap`; stop without changing strict aliases, retrieval, or ranking. | -| 8 | `#019` | A2 | Specialist — RAG answer pipeline | Local reproducer now; behavior change after `#051`/`#023` | 0.5–1 day reproducer | Reproduce admission-source loss in the fallback layer using PR #1096’s source shape. Any behavior change needs protected review and an approved baseline/post canary; stop if independently non-reproducible. | -| 9 | `#054` | A2 | Standard locally; Operator hosted | Local safety identifier now; hosted next approved window | 15–30 min local; 1–2 hours hosted | Presence-check and fill confirmed secret/config gaps with distinct per-environment values. Never record values. Require clean readiness/secret checks; stop on ambiguous environment or project identity. | -| 10 | `#022` | A2 | Operator — clinical governance + Specialist | Decision-ready | 1–2 hours policy; 0.5–1 day first ten | Decide BMJ attestation policy and review the ten highest-impact local documents. Record reviewer/evidence/time; stop after ten and remeasure warning debt. | -| 11 | `#051`, `#023` | A2 | Specialist — RAG diagnostics | After scheduled 2026-07-26 run | 2–4 hours | With GitHub-read approval, compare structured canary/browser/irrelevant-at-10 artifacts without dispatching a rerun. Record deterministic/provider/latency deltas and disposition residuals; stop without spending. | -| 12 | `#018` | A2 | Specialist — clinical RAG/retrieval | After `#051`/`#023`, one mechanism at a time | 1–2 days diagnosis | Give lithium, ADHD, and metabolic residuals separate current-main reproducers and candidates. Behavior canaries require approval; stop any item without a deterministic reproducer or on regression. | -| 13 | `#029` | A2 | Specialist — answer quality/clinical safety | After `#051`/`#023` and `#018` | 0.5–1 day inventory; 1–3 days per fix | Re-enumerate current fallback stubs and fix one causal cluster at a time without weakening grounding/citation gates. Stop if a change merely makes the metric easier to pass. | -| 14 | `#001` | A2 | Specialist — retrieval/ranking | After `#051`/`#023` and rollout approval | 0.5–1 day plus canary | Keep semantic reranking off unless an approved ambiguity comparison preserves 36/36, recall 1.0, zero per-case regressions, and shows measured gain; otherwise record keep-off and stop. | -| 15 | `#025` | A2 | Operator — Railway/GitHub/chat/Supabase | Next approved observability window | 1–3 hours/channel | Choose owned deployment, CI, ingestion, and SLO alerts; mock first, then one approved controlled provider event/channel. The merged Supabase trigger remains inert until its verified inputs are configured. Stop without an accountable responder. | -| 16 | `#055` | A2 | Specialist release owner + Operator | Before next full-confidence release/handoff | 2–4 hours plus runtime | On one exact SHA, run local/provider gates, Firefox/WebKit, required hosted CI, and close actionable GitHub threads. Stop at first failure and rerun only the repaired smallest gate. | -| 17 | `#056` | A2 | Operator — Supabase/Railway + Specialist | After cost/ownership approval | 0.5–1 day | Provision isolated `Clinical KB Staging` with synthetic data and distinct secrets. Verify identity, schema, indexing, health, and data boundary; never copy production clinical documents. | -| 18 | `#057` | A2 | High — release/SRE + Operator | After `#056` | 2–4 hours plus soak | Run documented staging soak and rollback against an exact candidate. Retain latency/error/rollback evidence; stop on unsafe data, identity mismatch, or unowned rollback. | -| 19 | `#058` | A2 | Operator — production data + Specialist | Next approved production verification window | 30–60 min read-only; 1–2 hours if needed | Verify registry/differentials/medications are non-empty before writing; seed only confirmed gaps idempotently. Stop when healthy or owner/project identity is ambiguous. | -| 20 | `#007` | A3 | Operator decision + Standard frontend | When product chooses canonical Tools route | 15–30 min decision; 0.5 day | Align navigation, redirect, sitemap, and reachability around one entry point. Stop while the standalone page has an unresolved requirement. | -| 21 | `#011` | A3 | Operator — Supabase capacity | Immediately before first compute scale-up | 30–60 min plus observation | Switch Auth to percentage allocation, record before/after, and run approved advisor/health checks. Stop if no scale-up is planned. | -| 22 | `#017` | A3 | High — performance/browser | Before `#012`/`#013`/`#016`; approved live-site window | 1–2 hours | Capture reproducible mobile/desktop Lighthouse/Web-Vitals evidence and decide whether payload work is justified. Stop if metrics are acceptable or evidence is too noisy. | -| 23 | `#024` | A3 | High — Next.js/Playwright/WebKit | After `#051`/`#023` or real Safari reproduction | 0.5–1 day | Distinguish test interception from a Safari defect. Apply test-only correction only with a discriminating repro; keep access-control assertions meaningful. | -| 24 | `#033` | A3 | Specialist — prompt/source governance | After `#022` and `#051`/`#023` | 1–2 days plus approved eval | Design unknown-vs-adverse metadata wording and prompt tests. Require no supported-grounding drop and zero citation failures; stop on broad over-caveating or degradation. | -| 25 | `#037` | A3 | Operator — clinical/product + Standard | Next trust-policy review | 30–60 min; up to 0.5 day | Decide whether routine claims cap at medium trust. Record policy; if accepted, change only the flag/expectations and run focused tests. | -| 26 | `#012`, `#013`, `#016` | A3 | High — bundling/runtime performance | After `#017` or equivalent evidence | 0.5–2 days/route | Optimize only a production route with measured payload/render/motion harm. Require material gain plus focused, `verify:cheap`, and browser evidence; stop on small gain. | -| 27 | `#035` | A3 | Specialist — evidence rules | After a demonstrated missed conflict | 0.5–1 day design; code separate | Define a clinically reviewed conflict class with positive and negative fixtures. Stop if no bounded class can be shown; behavior change requires protected review. | -| 28 | `#027` | Optional | Operator — SRE/provider | When an owned external alert path is wanted | 1–2 hours | Decide vendor/cost/privacy/owner; if accepted, prove one non-PHI outage and recovery alert. Stop when no responder owns it. | -| 29 | `#028` | Optional | Specialist privacy/observability + Operator | After privacy/ownership/cost approval | 1–3 days | Define vendor/region/retention/redaction/sampling/source-map envelope before SDK work. Prove no clinical text, identifiers, or secrets leave; stop if unacceptable. | -| 30 | `#038` | Optional | High — product/design architecture | When a new comparison surface is approved | 0.5–1 day | Define a shared interaction contract without flattening mode-specific content. Stop when no concrete new surface exists. | -| 31 | `#040` | Optional | High — visual QA/accessibility | When baseline owner/update workflow exists | 1–2 days | Establish a small stable desktop/mobile/accessibility baseline set. Do not make it blocking if flake or maintenance cost outweighs detection value. | -| 32 | `#039` | Optional | High — frontend architecture | During a concrete catalogue-toolbar project | 0.5–1 day inventory; 1–3 days code | Converge only repeated toolbar behavior without flattening search semantics. Stop when there is no bounded implementation target. | -| 33 | `#063` | A3 | High — product architecture + privacy | Only when the product owner wants to evaluate the feature | 0.5–1 day | Write a product/privacy/persistence brief for “Current Clinical Work” before storage or UI implementation. Stop if demand or safe persistence cannot be established. | - - -======= | Order | ID(s) | Acuity | Capability | When | Estimate | Outcome, gate, verification, and stopping condition | | ----: | ---------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 1 | `#059` | A1 | Operator security + independent reviewer | Immediate approved security window | 1–3 hours plus verification | Verify every reported exposed credential (GitHub, OpenAI, Supabase service role/database, E2E) is retired; rotate anything still valid and update only intended secret stores. Never record values; stop before provider action without approval. | @@ -123,7 +84,6 @@ removed after current-main verification; it is not missing recommended work. | 36 | `#065` | A2 | High — document-viewer UI | Only when the user explicitly resumes the paused task | 0.5–1.5 days | Finish the compact source-text accordion, citation/search auto-open, print restoration, and 320/390/1280 px coverage. Keep the preserved branch untouched until explicit resume; no provider calls. | ->>>>>>> theirs ## Open items @@ -131,53 +91,6 @@ removed after current-main verification; it is not missing recommended work. > **RAG reconciliation correction (2026-07-23):** fresh current-main live evidence supersedes the broad diagnosis in #018. The three named misses are not one composer defect. Lithium reproduced an unrelated-table retrieval fast-path defect; ADHD still retrieves a relevant chart-heavy CAMHS source but exhausts the extractive route budget; metabolic retrieves the correct AKG source but selects schedule-free prose; #019 remains post-retrieval comparison source selection. A narrow lithium subject-evidence guard improved its targeting result from 0 to 1 with golden recall 1.0 and no reciprocal-rank regressions, but was reverted and rejected because the required full canary failed. Keep #029 open for the remaining fallback-stub cases. Do not combine these residuals or change ranking scores, comparator ordering, aliases, clamps, or semantic reranking without a separate reproducer and passing canary pair. -<<<<<<< ours -| ID | Pri | Type | Summary | Detail / next action | Source | Added | -| ---- | --- | ----- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| #059 | P1 | task | Verify containment of the GitHub token exposed in chat | **Outcome:** the exposed token can no longer authenticate. **Next:** in an approved GitHub security window, revoke the old token, verify rejection without printing it, and create a replacement only if required through the intended credential store. **Success:** GitHub evidence shows the old token retired, any replacement is minimally scoped and stored only where intended, and secret scans remain clean. **Stop:** no provider or secret-store action without approval; never paste values into Git, logs, issues, or chat. | session 2026-07-24 security reconciliation | 2026-07-24 | -| #060 | P1 | issue | Safety Plan Generator contradicts the privacy contract | **Outcome:** the tool, privacy notice, PIA, and tests agree on whether patient identifiers may be entered, copied, printed, or saved. **Next:** obtain a privacy/clinical decision, then default to identifier-free behavior unless transient identifier processing is explicitly approved. **Success:** no contradictory copy and no identifier is persisted or transmitted without an approved basis. **Verify:** focused component/privacy/copy/accessibility tests, print/copy smoke, `verify:cheap`, and production-readiness. **Stop:** any new storage or provider transmission requires separate review. | `src/components/patient-safety-plan.tsx`; `src/app/privacy/page.tsx`; `docs/privacy-impact-assessment.md` | 2026-07-24 | -| #061 | P2 | issue | Missing answer relevance metadata is treated as source-backed | **Outcome:** absent `relevance` metadata renders conservatively. `RagAnswer.relevance` is optional, but `relevance?.isSourceBacked !== false` treats `undefined` as source-backed. **Next:** add the red render-policy test, then make the smallest policy-only fix. **Success:** missing and explicit-false relevance fail closed while explicit source-backed behavior is unchanged. **Verify:** focused render-policy/provenance/clinical-safety tests, `verify:cheap`, and production-readiness. **Stop:** do not expand into retrieval, ranking, or generation. | `src/lib/types.ts`; `src/lib/answer-render-policy.ts` | 2026-07-24 | -| #062 | P2 | issue | Upload crash can strand a queued document without a job | **Outcome:** a crash between document and job creation cannot strand an upload indefinitely. **Next:** add the stranded-row reproducer, then choose the smallest idempotent atomic-enqueue RPC or bounded scheduled sweep consistent with ownership and rollback contracts. **Success:** exactly one recoverable job is created, existing open jobs do not duplicate, and owner scope remains intact. **Verify:** focused upload/recovery/schema tests, migration guards, disposable replay if needed, `verify:cheap`, and production-readiness. **Stop:** hosted changes require approval; no at-least-once claim until the crash case passes. | `src/app/api/upload/route.ts`; `docs/webhooks.md` | 2026-07-24 | -| #063 | P3 | rec | Define “Current Clinical Work” before implementation | **Outcome:** decide whether a workspace combining saved comparisons, partial formulation work, recent tools, and pinned source sets is worth building. **Next:** write a product/privacy/persistence brief only; do not build storage or UI. **Success:** define users, data classes, lifecycle, cross-device expectations, deletion, failure states, demand evidence, and the smallest testable slice. **Stop:** close the idea if demand or safe persistence cannot be established. | session 2026-07-24; `src/lib/tools-catalog.ts` | 2026-07-24 | -| #051 | P2 | task | Stabilise the live answer-quality canary before more RAG tuning | Diagnostics landed in PR #1095: structured JSON/Markdown artifacts now record the actual checked-out SHA, run identity and latency context, and the offline trend tool separates content, provider-route and latency outcomes. First validating run `30018289898` recorded the expected tree and cost, with 36/36 retrieval green, but one report cannot establish variability; PR #1097 prevents a single failure being mislabeled as repeated. Next: compare the scheduled 2026-07-26 structured report with this run. Do not spend on an immediate retry or reapply the archived lithium guard before that comparison. | PR #1095; run `30018289898`; PR #1097; archive ref `refs/archive/rejected-rag/20260723/monitoring-subject-gate` | 2026-07-23 | -| #001 | P2 | task | Semantic reranking still gated off | `RAG_SEMANTIC_RERANK_ENABLED=false` from PR #901. Do not enable until the provider-backed 36/36 retrieval-quality gate **and** an ambiguity-focused canary are explicitly approved and recorded. | `docs/process-hardening.md` (Semantic reranking rollout debt); PR #901 | 2026-07-21 | -| #052 | P2 | issue | Reindex can overlap a fresh agent-enrichment pass | Full/retry single and bulk routes consult `ingestion_jobs` but not the implemented `hasActiveAgentEnrichmentJob` predicate. Add red route tests, then block fresh `indexing_v3_agent_jobs.status='processing'` leases before mutation while keeping stale leases and enrichment mode unchanged. | `src/lib/ingestion-mutation-safety.ts`; single/bulk reindex routes; repository audit 2026-07-24 | 2026-07-24 | -| #053 | P1 | task | Execute cross-border privacy/legal package | Execute OpenAI and Railway DPAs; decide ZDR and Australian data residency; obtain prompt-cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not represent the release as privacy-approved or alter final public privacy wording before sign-off. | `docs/openai-cross-border-basis.md`; `docs/privacy-impact-assessment.md` | 2026-07-24 | -| #054 | P2 | task | Reconcile local and hosted secrets/config | Presence-check safety-identifier, query-hash, deep-probe, Supabase service-role, OpenAI, project-identity, and schedule settings; set only confirmed gaps with distinct per-environment values. Never record secret values. Provider reads/writes require approval. | `.env.example`; production-readiness warning; `docs/operator-backlog.md` | 2026-07-24 | -| #055 | P2 | task | Run one exact-SHA full release and PR gate | Before the next full-confidence release/handoff, record the candidate/PR SHA and run the local/provider release gates, Firefox/WebKit, required hosted CI, and actionable GitHub review-thread closure once. Stop at the first actionable failure and rerun only the repaired smallest gate. | `docs/launch-operator-runbook.md`; `docs/codex-review-protocol.md` | 2026-07-24 | -| #056 | P2 | task | Provision isolated staging environment | After explicit cost/ownership approval, provision `Clinical KB Staging` Supabase and Railway tiers with distinct secrets and synthetic/non-clinical data. Verify identity, schema, indexing, health, and the production-data boundary. | `docs/staging-setup.md`; `docs/operator-backlog.md` | 2026-07-24 | -| #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | -| #058 | P2 | task | Verify production content before any seed write | Against `Clinical KB Database`, verify registry, differentials, and medications surfaces are non-empty before writing. Seed only confirmed gaps idempotently with approved owner/project identity and confirmation flags. | `docs/launch-operator-runbook.md`; `docs/operator-backlog.md` | 2026-07-24 | -| #005 | P3 | rec | `finalScore` saturates at clamp ceiling | Base + ~40 stacked boosts routinely exceed 1.0, so strong matches tie at 1.0 and order by an arbitrary `document_id` tiebreak. If ranking is ever revisited, break ties by the **pre-clamp** score rather than raising the `[0,1]` ceiling (downstream gates assume `[0,1]`). Ordering already sorts by the unbounded pre-clamp `rankScore` (`clinical-search.ts:1735,1927,1950-1955`), so the clamp confines only the reported confidence value, not result order. Not a defect on the current golden set; any change here is a protected RAG surface (canary required). | `docs/rag-hybrid-findings-and-todo.md` P1 item 4; `src/lib/clinical-search.ts:1735` | 2026-07-21 | -| #007 | P3 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | `/tools` (standalone `ApplicationsLauncherPage`) has no inbound in-app link; the sidebar Tools item uses `/?mode=tools`. Decide the canonical entry point and wire nav consistently, or drop the standalone `/tools` page + `/applications` redirect. Currently allowlisted in `tests/route-reachability.test.ts`. | `src/app/tools/page.tsx`; `src/app/applications/route.ts` | 2026-07-21 | -| #009 | P3 | rec | Confirm `/api/jobs` is intentionally server/ops-only | No client `fetch()` reaches `/api/jobs` (only tests import it). Confirm it is a deliberate ops/manual surface; if abandoned, remove it. | `src/app/api/jobs/route.ts` | 2026-07-21 | -| #010 | P3 | task | Un-built "Coming soon" controls across forms/favourites | ~10 disabled placeholders (forms refine/reset, favourites sort/add/new-set, move-to-set, remove-favourite). Correctly flagged (`aria-disabled` + "Coming soon"), not defects — wire when the underlying features land. | `forms-search-results-page.tsx`; `favourites-hub.tsx`; `favourites-command-library-page.tsx` | 2026-07-21 | -| #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up — **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | -| #012 | P3 | rec | Slim the lazy cross-mode differentials chunk | `cross-mode-differentials.ts` is dynamically imported (correctly code-split **out** of the initial/dashboard bundle — verified), but it pulls the full ~860 KB differentials snapshot (~125 KB gzip lazy chunk) just to build a tiny `{slug,title,clinicalHinge}` + presentations + aliases catalog. A precomputed lightweight index (generator + drift check, like the `specifiers-content` split / medications `fields=index`) would cut that lazy chunk ~5–10×. Not a bundle leak — an M-effort slim. | `src/lib/cross-mode-differentials.ts`; `src/components/clinical-dashboard/cross-mode-links.tsx:150`; session 2026-07-21 (build:analyze) | 2026-07-21 | -| #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | `build:analyze`: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), `/formulation` ships `formulation-content.json` (~52 KB, client-side local search — needs index/full split or a search endpoint, architectural). All route-scoped (not initial bundle). Also `*-mockups.tsx` (~100 KB across chunks) build though `/mockups` 404s in prod — exclude from the prod artifact. | session 2026-07-21 (build:analyze) | 2026-07-21 | -| #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `ƒ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them). | session 2026-07-21 (build route table + design audit) | 2026-07-21 | -| #017 | P3 | task | Field Web-Vitals baseline via live Lighthouse | In-sandbox runtime vitals were blocked (prod server hard-requires Supabase secrets; dev-mode CLS measured excellent at 0.00–0.04, content-first pages 0.000). Run Lighthouse against `psychiatry.tools` for real LCP/INP/CLS to prioritize #012–#016 by measured impact rather than reasoning. | session 2026-07-21 (measurement pass) | 2026-07-21 | -| #018 | P2 | task | Split the lithium, ADHD and metabolic residuals by mechanism | Revalidated on current main 2026-07-23: these are not one composer defect. Lithium reproduced an unrelated-table retrieval fast-path defect; ADHD retrieves a relevant chart-heavy CAMHS source but exhausts the extractive route budget; metabolic retrieves the correct AKG source but selects schedule-free prose. The narrow lithium subject-evidence guard improved targeting from 0 to 1 with golden recall 1.0 and no reciprocal-rank regressions, but it was reverted because the full canary failed. After #051 stabilises the canary, add independent current-main reproducers and assess each mechanism separately. Do not widen the matcher or combine these into a broad ranking/composer change. | runs `30007833352` and `30009207429`; PR #1093; session 2026-07-23 | 2026-07-21 | -| #019 | P2 | task | Admission doc dropped after deterministic comparison packing | Reconfirmed on merged-main run `30018289898`: golden retrieval remained 36/36 and retrieved `MHSP.AdmissionCommunityPts.pdf`, but the answer's top five sources retained only `MHSP.Discharge.pdf` after `generation_fallback:generation_quality_failed; comparison_source_extractive_fallback`. PR #1096 replays the live score/order shape and proves deterministic answer ranking plus cross-document packing retain both admission and discharge evidence, so retrieval scores, aliases and comparator ordering are not the fix. Next: create a red fallback-layer unit reproducer using the live source shape; any behavior change still needs the existing baseline and a passing post canary. | run `30018289898`; PR #1096; session 2026-07-23 | 2026-07-21 | -| #021 | P3 | rec | E-3d H2 residual: strong/comparison generation discards | approx. 6 generation attempts per full 44-case run still fail the final quality gate and fall to extractive on strong-route comparison/complex shapes (the designed-conservative outcome). PARKED: weakest cost/benefit on the queue — a wave (approx. $2-4 pair + reviewer cycle) to shave seconds off a few hard cases. Revisit only if latency/waste complaints or a cheaper lever appears. | E-3c design record; runs #59-#61 diagnostics | 2026-07-21 | -| #022 | P2 | task | Source-governance metadata refresh (operator) | **Worklist generated 2026-07-22 ($0, read-only): `docs/source-governance-refresh-worklist-2026-07-22.md`.** Reframed - this is NOT 59 clinical reviews. Of the 124 documents surfacing in canary top results, 59 are review-required, and **38 (64 pct) are the BMJ published-reference tier all sitting at `clinical_validation_status: unverified`** - one attestation-policy decision, not 38 reviews. The remaining 21 are genuine local WA health-service reviews (FSH 7, NMHS 4, CAMHS 3, AKG 2, KEMH 2, RPBG 2, RKPG 1), mostly `document_status: review_due`. Burn-down: top-10 documents clear 44 pct of flagged slots, top-20 clear 66 pct. Next: decide the BMJ attestation policy, then attest local docs by visibility (start `Clozapine Management by GP (NMHS)`, 22 slots at rank 1). | runs #61/#57 Source Governance data; `docs/source-governance-refresh-worklist-2026-07-22.md` | 2026-07-21 | -| #023 | P2 | task | Read Sunday 2026-07-26 scheduled-run artifacts | The 18:00 UTC scheduled runs deliver three free datapoints at once: first full-44 weekly canary (validates the #1044 ANSWER_CASE_LIMIT raise), browser-matrix flake second datapoint (webkit ui-route-coverage now reproduced + root-caused 2026-07-22 → see #024; firefox ui-formulation:91 still awaits a datapoint), and the irrelevant@10 labeling-audit artifact (§3.1 human-decision class). Read all three, then disposition. | sessions 2026-07-20/21; branch-review-ledger convergence notes | 2026-07-21 | -| #024 | P3 | issue | WebKit e2e `_rsc`-prefetch access-control-checks errors | verify:release:offline on `main` ce32fe170 (2026-07-22) reproduced #023's webkit clause: **6/6 deterministic** failures in `tests/ui-route-coverage.spec.ts` (Therapy Compass; DSM home/comparison; Specifier comparison/map; Differential stream), each a `pageerror … ?_rsc=… due to access control checks` on Next.js RSC prefetch — Chromium + Firefox clean. Not merge-blocking (required gate `test:e2e:pr` is chromium-only; the full webkit matrix is advisory/release-time). Most likely a Playwright route-interception × WebKit interaction, not a Safari user defect. Next: decide (a) allow/mock the `_rsc` routes for the `webkit` e2e project, or (b) confirm real Safari impact — before trusting the full-matrix webkit gate at release. NB the 2 other webkit fails (`ui-stress:412`, `ui-universal-search:210`) passed on isolated re-run = true flake. | session 2026-07-22 (verify:release:offline, `main` ce32fe170); refines #023 | 2026-07-22 | -| #025 | P2 | task | Activate the three webhooks (operator secrets) | Merged (#968/#1100) + deployed but inert — verified live: `POST /api/webhooks/railway` returns `503 webhook_not_configured`; the Supabase document-change trigger exists but lacks both activation inputs. To turn on: (1) Railway → set `RAILWAY_WEBHOOK_SECRET` + add the `?token=…` webhook URL; (2) set `SLACK_WEBHOOK_URL`/`DISCORD_WEBHOOK_URL` in BOTH the Railway **app/server env** and **GitHub repo secrets**; (3) set one matching document-change secret in the Railway app env as `SUPABASE_INGESTION_WEBHOOK_SECRET` and in Supabase Vault as `ingestion_webhook_secret`, then set the per-environment database GUC `app.ingestion_webhook_base_url` to the deployed app origin. Each path fails closed until fully configured, so this is pure ops. See `docs/webhooks.md` for verification and rotation. | sessions 2026-07-22/24; PRs #968/#1100; docs/webhooks.md | 2026-07-22 | -| #027 | P3 | rec | External uptime monitor independent of GitHub/Railway | `live-domain-monitor.yml` runs on GitHub's cron, so it won't run in exactly the outage it should catch (Actions or the deploy itself down). Add an off-platform synthetic monitor (UptimeRobot / Better Stack / Checkly) hitting `/api/health` with a webhook alert. Provider setup, not code. | session 2026-07-22 webhook review | 2026-07-22 | -| #028 | P3 | rec | Runtime error tracking (Sentry or similar) | No error tracking in the repo — production exceptions on `psychiatry.tools`, including how often `RAG_PROVIDER_MODE=auto` silently degrades to source-only, are invisible. Weigh adding `@sentry/nextjs` (dependency + DSN secret + instrumentation) vs cost; alert → chat/issue. Provider-backed; needs explicit sign-off before adding the dependency. | session 2026-07-22 webhook review | 2026-07-22 | -| #029 | P2 | issue | 12 of 30 answer-quality cases return the fallback stub | run #61 --dump-answers: 12/30 quality cases emit the source_backed_review_fallback boilerplate with answer_sections: [], all grounded with 4-6 citations. Some still PASS targeting because the stub echoes query keywords (the contraindication/document_lookup matchers need only a keyword), so the targeting metric MASKS the problem for those intents. Superset of #018 — fix in the extractive composer, validate with the provider-backed answer eval. | run #61 dump artifact; session 2026-07-22 | 2026-07-22 | -| #030 | P2 | issue | Wide-tier alias lets one doc satisfy both comparison slots | In src/lib/eval-document-matching.ts, "Admission to Discharge for Mental Health Inpatients" appears in BOTH the AdmissionCommunityPts and Discharge alias lists, so a single document can satisfy both expectedFiles slots and make allHit true — a latent false-pass on admission-discharge cases. Not firing today (that doc is not in the failing top-5) but it would mask a real miss. Tighten the tables so one doc cannot fill both sides. | src/lib/eval-document-matching.ts:32-65; session 2026-07-22 | 2026-07-22 | -| #032 | P3 | rec | Governance ranking weighting: REFUTED, not debt | The source-governance audit (PR #1051) flagged three "gaps": `review_due` carries no ranking penalty, `unknownCurrentnessPenalty` ships at 0, and `selectBestSourceRecommendation` ignores governance metadata. **These are deliberate, measured decisions — do NOT implement them as written.** Blanket metadata boosts/penalties in selection ordering were measured on 2026-07-02 to regress the golden retrieval eval to 16/23 (doc-recall@5 1.0→0.76, mrr 0.75→0.64). Two corpus facts make it unsafe: scores saturate at the clamp so stacked boosts fully override lexical relevance, and the corpus is only partially metadata-enriched while `normalizeSourceMetadata` coerces unenriched docs to `unknown`/`unverified` — so "unknown" ≠ "bad" and blanket weighting swings ranking approx. 0.35 for reasons unrelated to relevance. Even governance-as-tiebreak buried correct unenriched docs (3 designs bisected). Next action: none — treat as a guardrail. If ever revisited, RC8 (source-strength as a _filter_) is the tracked path, gated on `eval:retrieval:quality` 36/36 plus a live canary pair. | PR #118; `docs/rag-behaviour/refuted-approaches.md`; PR #1051 items 4/5/6 | 2026-07-22 | -| #033 | P3 | rec | Source governance metadata absent from the LLM prompt | `buildRagSourceBlock` omits `document_status`, `clinical_validation_status`, and `extraction_quality`, so the model cannot self-caveat during generation and governance is enforced only post-hoc. Generation-surface change: needs `eval:rag` plus `eval:quality --rag-only` (grounded-supported must not drop, citation-failure 0) and explicit approval. Carries the same "unknown ≠ bad" hazard as #032 — on a partially-enriched corpus the model would likely over-caveat correct sources, so design the prompt wording before spending an eval. | `src/lib/rag/rag-source-block.ts:126-198`; PR #1051 audit item 8 | 2026-07-22 | -| #035 | P3 | rec | Threshold-conflict detection covers only 3 params | `detectThresholdDisagreements` checks only ANC, WBC, and platelets paired with withholding verbs, so cross-source conflicts on medication doses, lithium/thyroid levels, or vital signs go undetected. Deliberately narrow (see the comment at `:469-474`). Broadening changes when an answer is classified `conflicting` and adds warnings — real false-positive risk. Needs new fixtures plus a behaviour review before any change. | `src/lib/evidence.ts:469-574`; PR #1051 audit item 7 | 2026-07-22 | -| #036 | P3 | rec | No explicit `is_public` visibility flag on documents | Public-corpus visibility is implicit: `owner_id IS NULL` on an `indexed` document (`resolveSearchScope`). The `metadata.public_corpus` marker is written by the promotion migrations but never used as a retrieval filter. Promotion is unconditional on `clinical_validation_status`, so unverified documents are publicly searchable — compensated by keeping `unverified_source` in the frontend-visible warning set. A hard schema flag touches RLS and the clinical-risk-gated retrieval RPCs; weigh against the existing compensating control before acting. | `supabase/schema.sql:61-108`; `src/lib/search-scope.ts:181-236`; PR #1051 audit item 3 | 2026-07-22 | -| #037 | P3 | rec | D5 trust-cap-all-claims flag parked OFF | `NEXT_PUBLIC_RAG_TRUST_CAP_ALL_CLAIMS` extends authority gating from high-risk claims to **all** supported claims (`deriveTrust`). Ships OFF by design; flipping it caps trust to `medium` for routine claims across the board — a product/clinical-UX decision, not a defect. Both states are test-pinned. Next action: product decision, then flip and re-baseline the UI expectations. | `src/lib/answer-render-policy.ts:159-177`; PR #1051 audit item 11 | 2026-07-22 | -| #038 | P3 | rec | Consolidate shared comparison behavior | Several clinical modes expose comparison workflows with similar selection, empty-state and mobile-dock needs. Define one shared behavioral contract before another comparison surface is added; keep mode-specific clinical content separate. This is a design-system recommendation, not a current defect. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | -| #039 | P3 | rec | Consolidate catalogue toolbar patterns | Catalogue/search pages have independently evolved filter, sort, result-count and mobile toolbar behavior. Inventory the existing implementations and converge only the repeated interaction contract; do not flatten mode-specific search semantics. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | -| #040 | P3 | rec | Add targeted visual-regression baselines | Keep a small approved baseline set for high-value desktop/mobile surfaces and accessibility modes instead of screenshotting every route. Start with account/settings, document viewer, mode homes and bottom-composer interactions; define an intentional-update workflow before enabling blocking comparisons. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | -| #041 | P3 | rec | Extend the existing Factsheets reading model | Do not add a second patient-facing Factsheets mode. Future patient-content work should extend the existing Easy Read/Standard presentation and its accessibility/content contracts. Revisit only with a concrete user need and source-governance plan. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | -======= | ID | Pri | Type | Summary | Detail / next action | Source | Added | | ---- | --- | ----- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | #059 | P1 | task | Verify containment of every credential reported exposed in chat | **Outcome:** every reported exposed credential is rejected or retired. **Next:** in approved security windows, verify and revoke or rotate the GitHub token, OpenAI key, Supabase service-role JWT, database password, and E2E credential; create replacements only when required and update only intended secret stores. **Success:** provider evidence confirms the old credentials cannot authenticate, replacements are distinct and minimally scoped, presence/readiness checks pass, and secret scans remain clean. **Stop:** no provider or secret-store action without approval; never print or paste values into Git, logs, issues, or chat. | session 2026-07-24 security reconciliation; AI Agent Target Manifest | 2026-07-24 | @@ -224,7 +137,6 @@ removed after current-main verification; it is not missing recommended work. | #039 | P3 | rec | Consolidate catalogue toolbar patterns | Catalogue/search pages have independently evolved filter, sort, result-count and mobile toolbar behavior. Inventory the existing implementations and converge only the repeated interaction contract; do not flatten mode-specific search semantics. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | | #040 | P3 | rec | Add targeted visual-regression baselines | Keep a small approved baseline set for high-value desktop/mobile surfaces and accessibility modes instead of screenshotting every route. Start with account/settings, document viewer, mode homes and bottom-composer interactions; define an intentional-update workflow before enabling blocking comparisons. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | | #041 | P3 | rec | Extend the existing Factsheets reading model | Do not add a second patient-facing Factsheets mode. Future patient-content work should extend the existing Easy Read/Standard presentation and its accessibility/content contracts. Revisit only with a concrete user need and source-governance plan. | design audit reconciliation; session 2026-07-22 | 2026-07-22 | ->>>>>>> theirs ## Resolved / archive @@ -232,14 +144,11 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | ID | Type | Summary | Outcome | Resolved | | ---- | ----- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -<<<<<<< ours -======= | #068 | task | Rework the unaccepted structured extractive fallback patch | Reverted the unaccepted `shouldPreserveStructuredExtractiveFallback` behavior and restored the generic source-backed review fallback routing/tests rather than layering more preservation logic on top. Focused RAG fallback tests pass. | 2026-07-24 | | #061 | issue | Missing answer relevance metadata is treated as source-backed | `deriveTrust` now treats missing `EvidenceRelevance` as not source-backed, capping the render model to low trust while explicit direct/partial source-backed relevance preserves existing behavior. Added render-policy coverage for absent relevance metadata. | 2026-07-24 | | #052 | issue | Reindex can overlap a fresh agent-enrichment pass | Full/retry single and bulk reindex safety checks now optionally query fresh `indexing_v3_agent_jobs.status=processing` leases and return a 409 `active_agent_enrichment` result before mutating queue state. Enrichment-mode RPC calls remain unchanged. | 2026-07-24 | | #062 | issue | Upload crash can strand a queued document without a job | Added service-role RPC `create_uploaded_document_with_ingestion_job(jsonb, integer)` and routed uploads through it so document row creation and initial ingestion job creation commit atomically. Upload cleanup still removes storage and the committed document on post-enqueue abort/failure. | 2026-07-24 | | #060 | issue | Safety Plan Generator contradicted the privacy contract | PR #1119 removed patient identifier entry, leaves the post-export name line blank, and aligned tool, privacy and PIA copy. DOM/privacy tests and Chromium copy/print/network coverage prove working content remains in React memory with no fetch/XHR; hosted Production UI, build, unit, policy, safety, static-analysis and secret checks passed. Support-contact details remain classified as sensitive local-only working content. | 2026-07-24 | ->>>>>>> theirs | #034 | issue | Answer cache can serve stale governance metadata | Current-source verification found direct route coverage already asserts RAG-cache invalidation on document PATCH, source review, label, bulk, and reindex mutation paths. The residual test recommendation is already met; changing the protected cache key is unnecessary. | 2026-07-24 | | #014 | rec | Realize the `next/image` win on signed previews | Superseded: `SignedImage` uses `next/image` for layout and sizing but deliberately sets `unoptimized`, preventing bearer signed URLs from entering the unauthenticated optimizer cache where cached content could outlive the token. No optimization task remains unless private-image delivery changes. | 2026-07-24 | | #026 | task | Wire the Supabase document-change trigger | PR #1100 merged after disposable PostgreSQL replay and hosted migration replay. Production migration history and read-only catalog proof confirm the enabled metadata trigger, security-definer function, pinned search path and denied anonymous/authenticated execution; `npm run check:drift` reports no unexpected live drift. Delivery remains intentionally inert until the operator inputs tracked in #025 are configured. | 2026-07-24 | diff --git a/scripts/check-docs-links.mjs b/scripts/check-docs-links.mjs index 6493a303e..c83e59538 100644 --- a/scripts/check-docs-links.mjs +++ b/scripts/check-docs-links.mjs @@ -100,7 +100,7 @@ function stripSuffixes(value) { if (result.startsWith("./")) result = result.slice(2); // Drop #anchor fragments and :line / :line-line / :line:col suffixes. result = result.replace(/#[^#]*$/, ""); - result = result.replace(/:\d+([-:]\d+)?$/, ""); + result = result.replace(/:[L]?\d+([-:][L]?\d+)?$/i, ""); return result; } diff --git a/scripts/decompose-indexing-v3.mjs b/scripts/decompose-indexing-v3.mjs index b5c8e22cc..476358fca 100644 --- a/scripts/decompose-indexing-v3.mjs +++ b/scripts/decompose-indexing-v3.mjs @@ -1,5 +1,4 @@ import fs from "fs"; -import path from "path"; const indexFile = "supabase/functions/indexing-v3-agent/index.ts"; const utilsFile = "supabase/functions/indexing-v3-agent/utils.ts"; diff --git a/src/app/api/answer/route.ts b/src/app/api/answer/route.ts index 306a13640..cd7ee30fe 100644 --- a/src/app/api/answer/route.ts +++ b/src/app/api/answer/route.ts @@ -3,19 +3,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { demoAnswer, demoSummary } from "@/lib/demo-data"; import { isDemoMode } from "@/lib/env"; -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -import { answerQuestionWithScope } from "@/lib/rag/rag"; -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs +import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag/rag"; import { jsonError, PublicApiError } from "@/lib/http"; import { allowRateLimitInMemoryFallbackOnUnavailable, diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 4d92aae2c..f4a8d6f8d 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -225,9 +225,9 @@ export async function POST(request: Request) { content_hash: contentHash, metadata: { source_title: title, - publisher_code: null, - publisher: null, - jurisdiction: "Australia/WA", + publisher_code: canonicalAuthority ? (identityAuthority.code ?? canonicalAuthority.codes[0] ?? null) : null, + publisher: canonicalAuthority?.publisher ?? null, + jurisdiction: canonicalAuthority?.jurisdictions[0] ?? "Australia/WA", version: null, publication_date: null, review_date: null, @@ -246,36 +246,6 @@ export async function POST(request: Request) { max_upload_mb: env.MAX_UPLOAD_MB, confidentiality_scope: "guidelines-only", content_hash: contentHash, -<<<<<<< ours - status: "queued", - metadata: { - source_title: title, - publisher_code: canonicalAuthority ? (identityAuthority.code ?? canonicalAuthority.codes[0] ?? null) : null, - publisher: canonicalAuthority?.publisher ?? null, - jurisdiction: canonicalAuthority?.jurisdictions[0] ?? "Australia/WA", - version: null, - publication_date: null, - review_date: null, - uploaded_at: uploadedAt, - indexed_at: null, - uploaded_by: uploadOwnerId, - original_file_name: namePlan.originalFileName, - original_title: namePlan.originalTitle, - smart_title_base: namePlan.baseTitle, - smart_title_group_key: namePlan.duplicateGroupKey, - smart_title_duplicate_index: namePlan.duplicateIndex, - smart_title_duplicate_reason: namePlan.duplicateReason, - document_status: "unknown", - clinical_validation_status: "unverified", - extraction_quality: "unknown", - max_upload_mb: env.MAX_UPLOAD_MB, - confidentiality_scope: "guidelines-only", - content_hash: contentHash, - }, - }) - .select() - .single(); -======= }, }; @@ -287,7 +257,6 @@ export async function POST(request: Request) { p_max_attempts: env.WORKER_MAX_ATTEMPTS, }, ); ->>>>>>> theirs if (uploadRecordError) { if (isContentHashDuplicateError(uploadRecordError)) { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 993f53282..c7805b075 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -19,7 +19,6 @@ import { ShieldAlert, Square, UploadCloud, - WifiOff, Wrench, } from "lucide-react"; import { @@ -62,6 +61,7 @@ import { ClinicalDesktopSidebar, ClinicalMobileSidebar, } from "@/components/clinical-dashboard/ClinicalSidebar"; +import { LibraryHealthStrip, fallbackSetupChecks, hasReadyRequiredPublicSearchConfig, diff --git a/src/components/calculator-mockups/calculator-fixtures.ts b/src/components/calculator-mockups/calculator-fixtures.ts index 2139015bc..6b123d9f0 100644 --- a/src/components/calculator-mockups/calculator-fixtures.ts +++ b/src/components/calculator-mockups/calculator-fixtures.ts @@ -622,7 +622,7 @@ export const calculators: CalculatorFixture[] = [ }, ]; -const calculatorById = (id: string): CalculatorFixture => { +export const calculatorById = (id: string): CalculatorFixture => { const found = calculators.find((calc) => calc.id === id); if (!found) throw new Error(`Unknown calculator fixture: ${id}`); return found; diff --git a/src/components/calculator-mockups/calculator-ui.tsx b/src/components/calculator-mockups/calculator-ui.tsx index 823ad5853..93446bc7d 100644 --- a/src/components/calculator-mockups/calculator-ui.tsx +++ b/src/components/calculator-mockups/calculator-ui.tsx @@ -167,7 +167,7 @@ function selectOptionAnswer(answers: AnswerMap, itemId: string, optionIndex: num return { ...answers, [itemId]: answers[itemId] === optionIndex ? undefined : optionIndex }; } -function useCalculatorState(calc: CalculatorFixture): CalculatorState { +export function useCalculatorState(calc: CalculatorFixture): CalculatorState { const [answers, setAnswers] = useState({}); const toggleCheckbox = useCallback((itemId: string) => { diff --git a/src/components/clinical-dashboard/answer-status.tsx b/src/components/clinical-dashboard/answer-status.tsx index 559d3791e..679a67fa1 100644 --- a/src/components/clinical-dashboard/answer-status.tsx +++ b/src/components/clinical-dashboard/answer-status.tsx @@ -162,24 +162,9 @@ export function AnswerProgressStepper({ active: boolean; onStop: () => void; }) { -<<<<<<< ours -<<<<<<< ours const [now, setNow] = useState(() => Date.now()); const latest = events.at(-1) ?? null; const finished = latest?.stage === "complete"; -======= -======= ->>>>>>> theirs - const latest = events.at(-1) ?? null; - const finished = latest?.stage === "complete"; - const now = useClientTime({ - fallback: startedAt ?? 0, - updateInterval: active && !finished && startedAt ? 1_000 : undefined, - }); -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs const currentStep = latest ? answerProgressStepIndex(latest.stage) : 0; useEffect(() => { diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx index ed597c8a8..346501ad6 100644 --- a/src/components/clinical-dashboard/evidence-panels.tsx +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -575,15 +575,13 @@ function clinicalNotesAvailableTabs(sections: ClinicalDetailSection[]) { } /** -<<<<<<< ours -======= * Align clinical-notes inputs with the fail-closed render model: when an answer * is not explicitly source-backed, strip structured clinical payloads so the * notes sheet cannot reconstruct actionable monitoring/escalation/comparison * content from untrusted sections, quotes, or documentBreakdown (visual * evidence is passed separately). */ -function trustGatedAnswerForClinicalNotes( +export function trustGatedAnswerForClinicalNotes( answer: RagAnswer, visualEvidence: VisualEvidenceCard[] = answer.visualEvidence ?? [], ): RagAnswer { diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index 2cfff3194..1ee287803 100644 --- a/src/components/clinical-dashboard/settings-dialog.tsx +++ b/src/components/clinical-dashboard/settings-dialog.tsx @@ -50,7 +50,6 @@ import { fieldControlWithIcon, fieldIcon, floatingControl, - IconButton, InlineNotice, primaryControl, toggleThumbSurface, @@ -261,19 +260,7 @@ export function SettingsDialog({ return () => window.cancelAnimationFrame(focusFrame); }, [emailEntryOpen]); -<<<<<<< ours const backButton = ; -======= - const backButton = ( - - ); ->>>>>>> theirs const closeButton = (