From 2646f517e6962c77bc02b4c0aad65c668b44d2e6 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:58:45 +0800 Subject: [PATCH 1/3] fix(rag): require co-located medication evidence --- src/lib/clinical-search.ts | 64 +++++++++++++++++++-- src/lib/rag.ts | 49 +++++++++++----- tests/retrieval-query-variants.test.ts | 78 +++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 20 deletions(-) diff --git a/src/lib/clinical-search.ts b/src/lib/clinical-search.ts index ca3d3f091..368519215 100644 --- a/src/lib/clinical-search.ts +++ b/src/lib/clinical-search.ts @@ -749,7 +749,7 @@ function evidenceDensityBoost(result: SearchResult, tokens: string[]) { /** Has dose evidence support. */ export function hasDoseEvidenceSupport(result: SearchResult) { const haystack = clinicalResultEvidenceHaystack(result); - return /\b(?:dose|dosage|dosing|mg|mcg|microgram|route|oral|intramuscular|subcutaneous|subcut|sublingual|\bim\b|\bpo\b|\bsc\b|\bsl\b|\bprn\b|administer\w*|titration|titrate|frequency|maximum|tablet|injection|antipsychotic|benzodiazepine|olanzapine|lorazepam|haloperidol|droperidol|promethazine|diazepam)\b/i.test( + return /\b(?:dose|dosage|dosing|mg|mcg|micrograms?|milligrams?|ug|route|oral|intramuscular|subcutaneous|subcut|sublingual|\bim\b|\bpo\b|\bsc\b|\bsl\b|\bprn\b|administer\w*|titration|titrate|frequency|maximum|tablet|injection|antipsychotic|benzodiazepine|olanzapine|lorazepam|haloperidol|droperidol|promethazine|diazepam)\b|[µμ]g/i.test( haystack, ); } @@ -757,7 +757,7 @@ export function hasDoseEvidenceSupport(result: SearchResult) { /** Has medication dose amount evidence. */ function hasMedicationDoseAmountEvidence(result: SearchResult) { const haystack = clinicalResultEvidenceHaystack(result); - return /\b\d+(?:\.\d+)?\s?(?:mg|mcg|microgram|micrograms)\b/i.test(haystack); + return /\b\d+(?:\.\d+)?\s?(?:mg|mcg|micrograms?|milligrams?|ug|[µμ]g)\b/i.test(haystack); } // A passage carrying a real dose/threshold figure (a numeric table row, or a @@ -1105,30 +1105,69 @@ export function normalizedClinicalSearchTokens(query: string) { const genericMedicationDoseQueryTokens = new Set([ "administer", + "administered", + "administering", "administration", + "amount", "chart", "dose", "dosage", "dosing", "drug", "frequency", + "frequent", + "frequently", + "given", "guidance", "listed", "management", + "many", + "max", "maximum", "medication", "medicine", + "mcg", + "mg", + "microgram", + "milligram", + "min", + "minimum", + "much", + "often", "oral", + "orally", "intramuscular", + "intramuscularly", "subcutaneous", + "subcutaneously", "subcut", "sublingual", + "sublingually", "route", "shown", "table", "used", "using", "usual", + "ug", + "once", + "twice", + "daily", + "nightly", + "weekly", + "monthly", + "hourly", + "prn", + "bd", + "tds", + "qds", + "qid", + "every", + "hour", + "day", + "week", + "time", + "mouth", "im", "po", "patient", @@ -1144,11 +1183,26 @@ export function medicationDoseQuerySubjectTokens(query: string) { return normalizedClinicalSearchTokens(query).filter((token) => !genericMedicationDoseQueryTokens.has(token)); } +const medicationDoseAmountQueryPattern = + /\b(?:dose|doses|dosage|dosages|dosing|amount|amounts|maximum|max|min|minimum|mg|mcg|micrograms?|milligrams?|ug)\b|[µμ]g|\bhow\s+much\b/i; +const medicationDoseRouteQueryPattern = + /\b(?:route|routes|oral|orally|intramuscular|intramuscularly|subcutaneous|subcutaneously|subcut|sublingual|sublingually|im|po|sc|sl|by\s+mouth)\b/i; +const medicationDoseFrequencyQueryPattern = + /\b(?:frequency|frequencies|frequent|frequently|how\s+often|how\s+frequently|once|twice|daily|nightly|weekly|monthly|hourly|prn|bd|tds|qds|qid|every\s+\d+(?:\.\d+)?\s*(?:hours?|days?|weeks?)|\d+\s+times?\s+(?:a|per)\s+(?:day|week|hour))\b/i; + +/** Which explicit medication evidence attributes the query requests. */ +export function medicationDoseEvidenceQueryIntent(query: string) { + return { + asksAmount: medicationDoseAmountQueryPattern.test(query), + asksRoute: medicationDoseRouteQueryPattern.test(query), + asksFrequency: medicationDoseFrequencyQueryPattern.test(query), + }; +} + /** Whether the query explicitly asks for dose, route, or frequency evidence. */ export function isMedicationDoseEvidenceQuery(query: string) { - return /\b(?:dose|doses|dosage|dosages|dosing|route|oral|intramuscular|subcutaneous|subcut|sublingual|im|po|sc|sl|frequency|mg|mcg|microgram|maximum|minimum|prn)\b/i.test( - query, - ); + const intent = medicationDoseEvidenceQueryIntent(query); + return intent.asksAmount || intent.asksRoute || intent.asksFrequency; } /** Require dose evidence to carry the medication question's clinical subject. */ diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 9c50cd178..122c3644d 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -111,6 +111,7 @@ import { hasDoseEvidenceSupport, hasStructuredThresholdEvidence, isMedicationDoseEvidenceQuery, + medicationDoseEvidenceQueryIntent, medicationDoseQueryContext, normalizedClinicalSearchTokens, rankClinicalResults, @@ -2761,7 +2762,7 @@ function hasRiskFlowchartActionEvidence(query: string, results: SearchResult[], /** Has dose amount evidence for gate. */ function hasDoseAmountEvidenceForGate(result: SearchResult) { - return /\b\d+(?:\.\d+)?\s?(?:mg|mcg|microgram|micrograms)\b/i.test(evidenceTextForGate(result)); + return /\b\d+(?:\.\d+)?\s?(?:mg|mcg|micrograms?|milligrams?|ug|[µμ]g)\b/i.test(evidenceTextForGate(result)); } /** Has route evidence for gate. */ @@ -2771,6 +2772,13 @@ function hasRouteEvidenceForGate(result: SearchResult) { ); } +/** Has administration frequency evidence for gate. */ +function hasFrequencyEvidenceForGate(result: SearchResult) { + return /\b(?:once|twice|daily|nightly|weekly|monthly|hourly|prn|bd|tds|qds|qid|every\s+\d+(?:\.\d+)?\s*(?:hours?|days?|weeks?)|\d+\s+times?\s+(?:a|per)\s+(?:day|week|hour))\b/i.test( + evidenceTextForGate(result), + ); +} + /** Has direct source image evidence. */ function hasDirectSourceImageEvidence(result: SearchResult) { const sourceImageIds = new Set( @@ -2926,9 +2934,7 @@ export function evaluateEvidenceCoverageGate( } if (queryClass === "medication_dose_risk") { - const asksDoseAmount = /\b(?:dose|doses|dosage|dosages|dosing|mg|mcg|microgram|maximum|minimum)\b/i.test(query); - const asksRoute = - /\b(?:route|oral|intramuscular|subcutaneous|subcut|sublingual|\bim\b|\bpo\b|\bsc\b|\bsl\b)\b/i.test(query); + const { asksAmount, asksRoute, asksFrequency } = medicationDoseEvidenceQueryIntent(query); const agitationOk = !/\bagitation|arousal\b/i.test(query) || /\bagitation|arousal\b/i.test(evidenceText); const hasContextualDoseEvidence = top.some( (result) => hasDoseEvidenceSupport(result) && medicationDoseQueryContext(query, result).matched, @@ -2945,24 +2951,39 @@ export function evaluateEvidenceCoverageGate( hasRouteEvidenceForGate(result) && medicationDoseQueryContext(query, result).matched, ); - const accepted = - hasContextualDoseEvidence && - (!asksDoseAmount || hasContextualDoseAmount) && - (!asksRoute || hasContextualRoute) && - agitationOk; + const hasContextualFrequency = top.some( + (result) => + hasDoseEvidenceSupport(result) && + hasFrequencyEvidenceForGate(result) && + medicationDoseQueryContext(query, result).matched, + ); + const hasCoLocatedRequestedEvidence = top.some( + (result) => + hasDoseEvidenceSupport(result) && + medicationDoseQueryContext(query, result).matched && + (!asksAmount || hasDoseAmountEvidenceForGate(result)) && + (!asksRoute || hasRouteEvidenceForGate(result)) && + (!asksFrequency || hasFrequencyEvidenceForGate(result)), + ); + const requestedAttributeCount = Number(asksAmount) + Number(asksRoute) + Number(asksFrequency); + const accepted = hasCoLocatedRequestedEvidence && agitationOk; return { accepted, reason: accepted ? "dose_route_amount_evidence_gate" - : !hasDoseAmount + : asksAmount && !hasDoseAmount ? "missing_dose_amount_evidence" - : !hasContextualDoseEvidence || (asksDoseAmount && !hasContextualDoseAmount) + : !hasContextualDoseEvidence || (asksAmount && !hasContextualDoseAmount) ? "missing_dose_query_context" : !hasContextualRoute && asksRoute ? "missing_route_evidence" - : !agitationOk - ? "missing_agitation_context" - : "missing_dose_evidence", + : !hasContextualFrequency && asksFrequency + ? "missing_frequency_evidence" + : requestedAttributeCount > 1 && !hasCoLocatedRequestedEvidence + ? "missing_co_located_medication_evidence" + : !agitationOk + ? "missing_agitation_context" + : "missing_dose_evidence", strategy: "text_fast_path", sourceImageRequired, sourceImageSatisfied, diff --git a/tests/retrieval-query-variants.test.ts b/tests/retrieval-query-variants.test.ts index f8a20fc45..9fcaa4660 100644 --- a/tests/retrieval-query-variants.test.ts +++ b/tests/retrieval-query-variants.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "vitest"; -import { analyzeClinicalQuery, buildClinicalTextSearchQuery, rankClinicalResults } from "../src/lib/clinical-search"; +import { + analyzeClinicalQuery, + buildClinicalTextSearchQuery, + isMedicationDoseEvidenceQuery, + medicationDoseEvidenceQueryIntent, + rankClinicalResults, +} from "../src/lib/clinical-search"; import { expandClinicalVocabularyText } from "../src/lib/clinical-vocabulary"; import { selectRetrievalEvidence } from "../src/lib/retrieval-selection"; import { @@ -310,6 +316,53 @@ describe("retrieval query variants", () => { ).toEqual({ returnFastPath: false, reason: "missing_dose_query_context" }); }); + it.each([ + ["How much lorazepam should be given?", { asksAmount: true, asksRoute: false, asksFrequency: false }], + ["How many micrograms of clonidine are used?", { asksAmount: true, asksRoute: false, asksFrequency: false }], + ["Is clonidine 100 µg listed?", { asksAmount: true, asksRoute: false, asksFrequency: false }], + ["How often should lorazepam be administered?", { asksAmount: false, asksRoute: false, asksFrequency: true }], + ["Is olanzapine administered intramuscularly?", { asksAmount: false, asksRoute: true, asksFrequency: false }], + ])("detects explicit medication evidence intent in %s", (query, expected) => { + expect(isMedicationDoseEvidenceQuery(query)).toBe(true); + expect(medicationDoseEvidenceQueryIntent(query)).toEqual(expected); + }); + + it("routes natural amount and frequency questions through the contextual gate", () => { + expect( + decideTextFastPath( + "How much lorazepam should be given?", + [result({ content: "Lorazepam may be used with clinical review.", similarity: 0.9 })], + "medication_dose_risk", + ), + ).toEqual({ returnFastPath: false, reason: "missing_dose_amount_evidence" }); + + expect( + decideTextFastPath( + "How often should lorazepam be administered?", + [result({ content: "Lorazepam 1 mg may be used with clinical review.", similarity: 0.9 })], + "medication_dose_risk", + ), + ).toEqual({ returnFastPath: false, reason: "missing_frequency_evidence" }); + + expect( + decideTextFastPath( + "How often should lorazepam be administered?", + [result({ content: "Lorazepam 1 mg may be administered every 6 hours.", similarity: 0.9 })], + "medication_dose_risk", + ), + ).toEqual({ returnFastPath: true, reason: "dose_evidence_text_match" }); + }); + + it("accepts microgram-symbol dose evidence when it is co-located with the subject", () => { + expect( + evaluateEvidenceCoverageGate( + "How many micrograms of clonidine are used?", + [result({ content: "Clonidine 100 µg may be used with monitoring.", similarity: 0.9 })], + "medication_dose_risk", + ), + ).toMatchObject({ accepted: true, reason: "dose_route_amount_evidence_gate" }); + }); + it("keeps flowchart zone-action fast paths gated on action evidence", () => { const query = "In the clinical flowchart, what is the next step after red-zone risk?"; @@ -690,6 +743,29 @@ describe("retrieval query variants", () => { ).toMatchObject({ accepted: true, reason: "dose_route_amount_evidence_gate" }); }); + it("requires requested dose and route evidence to be co-located", () => { + expect( + evaluateEvidenceCoverageGate( + "How much lorazepam is administered intramuscularly?", + [ + result({ id: "amount", content: "Lorazepam 1 mg may be used with monitoring.", similarity: 0.9 }), + result({ id: "route", content: "Lorazepam may be administered intramuscularly.", similarity: 0.88 }), + ], + "medication_dose_risk", + ), + ).toMatchObject({ accepted: false, reason: "missing_co_located_medication_evidence" }); + }); + + it("reports missing route evidence for route-only questions", () => { + expect( + evaluateEvidenceCoverageGate( + "Which route is listed for lorazepam?", + [result({ content: "Lorazepam is recommended with clinical monitoring.", similarity: 0.9 })], + "medication_dose_risk", + ), + ).toMatchObject({ accepted: false, reason: "missing_route_evidence" }); + }); + it("accepts SC and SL route evidence for dose-route fast gates", () => { expect( evaluateEvidenceCoverageGate( From 89ea74b97624aac1af2b0068b446872b4158f2f8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:26:09 +0800 Subject: [PATCH 2/3] docs: record RAG review follow-up --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b269b7eb8..1b290e2e1 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -53,3 +53,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-13 | codex/repository-review-remediation | 452275824294564a1e08e6bec169bd4af744d09a | live migration apply and post-apply review | Applied the four reviewed forward migrations to `Clinical KB Database`, aligned repository filenames to the generated production versions, and corrected the schema snapshot so the legacy unfenced commit overload remains inaccessible to `service_role`. Live drift is clean and no active ingestion/enrichment overlap or duplicate open ingestion group was found. | Ran `npm run check:drift`: passed clean. Ran `npm run check:production-readiness`: READY. Ran Docker schema replay: passed. Ran focused concurrency/retrieval Vitest: 166/166 passed. Ran offline RAG: 36 fixtures and 60/60 contract tests passed. Ran M13, retrieval-owner, schema-health, lexical-retrieval, concurrency, and ACL live probes: passed; lexical retrieval returned 12 truthfully scored results. Not completed: full provider retrieval-quality evaluation exceeded the local command window; deterministic live retrieval checks passed. | | 2026-07-13 | codex/fix-48h-review-findings-current | 49735663370735a60870d065ed0de3b9d34e077f | last-48-hours PR remediation | Revalidated the last-48-hours findings on current main after PRs #538 and #540; retained only unique fixes across auth/cache isolation, stale-response protection, upload/routing/UI behavior, RAG coalescing, telemetry, worktree tooling, and SAST enforcement. No remaining high-confidence local defect was found in the changed scope. The approved live drift check reported only the five differences already explained by unapplied migrations from #540. | Focused Vitest 107/107; `npm run verify:pr-local` (1,762 passed, 1 skipped; production build and client-bundle scan; offline RAG 60/60); critical Chromium 8/8; live `check:drift`; `git diff --check`. Full Chromium remains advisory after the earlier runner hang; the required critical subset passed on current main. | | 2026-07-13 | codex/public-anonymous-access | 7f3eded3d17c9daf6a443c9cac3f0553e4e9321b | production UI design and accessibility review | Fixed the fullscreen clinical-table focus leak and divergent modal implementation, removed the non-native table-surface control, and lifted meaningful production metadata from 8-10px to the 11px floor with stronger muted contrast. No remaining high-confidence defect was found in the reviewed visual scope. | Baseline/final screenshots at 1440x1000 and 390x820; focused Chromium table expansion 3/3; focused Vitest 6/6; `npm run typecheck`; targeted ESLint; type-scale and focused Prettier checks; `git diff --check`. `verify:cheap` timed out in full lint/test execution; full `verify:ui` deferred under the API confirmation boundary. | +| 2026-07-13 | codex/rag-review-followup | a4b1c58ccbcf57f7a6ddd495c9217bce5544cccf | PR #575 post-merge review finding remediation | Fixed the P1 path that could combine a medication amount and route from separate chunks, expanded the shared explicit amount/route/frequency intent detector, corrected route-only failure classification, and added microgram-symbol coverage. Requested attributes must now be co-located with the medication subject before the text fast path is accepted. No additional high-confidence defect was found in the changed scope. | Focused Vitest 143/143; `npm run eval:rag:offline` (21 files, 265/265); `npm run typecheck`; targeted ESLint; full `npm test` (211 files passed, 1 skipped; 1,946 tests passed, 1 skipped); PR-local dry-run selected runtime, format, lint, typecheck, full tests, build, and offline RAG; `git diff --check`. `verify:cheap` passed all pre-test stages but its 10-minute host bound expired during the full suite; the same suite then passed independently with a longer bound. | From ea56104cd658217d67a0a768ec250943bdddd8e8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:46:03 +0800 Subject: [PATCH 3/3] docs: refresh RAG review integration record --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index e05bc3c5a..4e2653da0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -54,3 +54,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-13 | codex/fix-48h-review-findings-current | 49735663370735a60870d065ed0de3b9d34e077f | last-48-hours PR remediation | Revalidated the last-48-hours findings on current main after PRs #538 and #540; retained only unique fixes across auth/cache isolation, stale-response protection, upload/routing/UI behavior, RAG coalescing, telemetry, worktree tooling, and SAST enforcement. No remaining high-confidence local defect was found in the changed scope. The approved live drift check reported only the five differences already explained by unapplied migrations from #540. | Focused Vitest 107/107; `npm run verify:pr-local` (1,762 passed, 1 skipped; production build and client-bundle scan; offline RAG 60/60); critical Chromium 8/8; live `check:drift`; `git diff --check`. Full Chromium remains advisory after the earlier runner hang; the required critical subset passed on current main. | | 2026-07-13 | codex/public-anonymous-access | 7f3eded3d17c9daf6a443c9cac3f0553e4e9321b | production UI design and accessibility review | Fixed the fullscreen clinical-table focus leak and divergent modal implementation, removed the non-native table-surface control, and lifted meaningful production metadata from 8-10px to the 11px floor with stronger muted contrast. No remaining high-confidence defect was found in the reviewed visual scope. | Baseline/final screenshots at 1440x1000 and 390x820; focused Chromium table expansion 3/3; focused Vitest 6/6; `npm run typecheck`; targeted ESLint; type-scale and focused Prettier checks; `git diff --check`. `verify:cheap` timed out in full lint/test execution; full `verify:ui` deferred under the API confirmation boundary. | | 2026-07-13 | main (PR #570 squash, glass header) | cc6bfc1c80902ca3c91e5ba2ebe78a80f3fd9e14 | post-merge review: CSS/visual/a11y/perf + logic/regression | No P0/P1. P2s confirmed and fixed in follow-up: build pipeline dropped ALL hand-authored backdrop-filter declarations (manual -webkit- duplicates confused Lightning CSS — header scrim, bottom dock, and composer pill were tint-only in every engine); scrim retuned to carry the bar's frost alone (header backdrop-root removed) with masks fading to true zero; private-scope alert made sticky inside main (was scroll-away in non-answer modes); scroll-hide reporter reset on breakpoint-gate change; non-forced fallback backgrounds layered to preserve the utility-wins contract. | Ran custom Playwright probe (`node scratchpad/probe-blur.mjs`, Chromium 390x844): baseline showed `getComputedStyle(.edge-glass-header-backdrop).backdropFilter === "none"` on all three passes, after fix `blur(14px)/blur(20px)/blur(26px)` — passed; Ran `node scripts/run-playwright.mjs tests/ui-smoke.spec.ts --project=chromium -g "glass header\|collapse hide\|private-scope alert\|phone (short\|long) answer stays"`: 6/6 passed; Ran `node scripts/run-playwright.mjs tests/ui-smoke.spec.ts --project=chromium` (full file): 71 passed, 4 failed (pre-existing `/privacy` heading test, fails identically on clean main); Ran `npm run verify:cheap`: exit 0 (1935 unit tests passed); Ran `npm run format:check`: passed; Not run: WebKit/Safari real-device check (no WebKit runner in this environment — served client chunk verified to pair `-webkit-backdrop-filter` with each declaration for Safari <= 17) | +| 2026-07-13 | codex/rag-review-followup | 755ac9e517a3b81f8e12a119f80f3769dd58ae4e | PR #575 post-merge review finding remediation | Fixed the P1 path that could combine a medication amount and route from separate chunks, expanded the shared explicit amount/route/frequency intent detector, corrected route-only failure classification, and added microgram-symbol coverage. Requested attributes must now be co-located with the medication subject before the text fast path is accepted. No additional high-confidence defect was found in the changed scope after integrating the production answer-budget fix from PR #580. | Focused Vitest 143/143; `npm run eval:rag:offline` (21 files, 265/265); `npm run typecheck`; targeted ESLint; full `npm test` (211 files passed, 1 skipped; 1,946 tests passed, 1 skipped); PR-local dry-run selected runtime, format, lint, typecheck, full tests, build, and offline RAG; `git diff --check`. `verify:cheap` passed all pre-test stages but its 10-minute host bound expired during the full suite; the same suite then passed independently with a longer bound. |