Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
881c51e
fix: close release governance and offline readiness gaps
BigSimmo Jul 14, 2026
1ca2f9f
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
c4f16d5
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
c8a3dd1
fix: allow explicit offline production startup
BigSimmo Jul 14, 2026
409d6e8
Merge branch 'main' into codex/release-blocker-remediation
Copilot Jul 14, 2026
bca4734
fix: preserve public registry governance scope
BigSimmo Jul 14, 2026
3c6cf9d
Merge remote-tracking branch 'origin/codex/release-blocker-remediatio…
BigSimmo Jul 14, 2026
98aaad4
test: anchor staging tenancy retrieval fixture
BigSimmo Jul 14, 2026
3b152ed
test: read document IDs in tenancy evidence
BigSimmo Jul 14, 2026
4d55c3d
fix: preserve registry scope during refresh
BigSimmo Jul 14, 2026
978d4f4
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
dedb38a
fix: close final offline and governance review gaps
BigSimmo Jul 14, 2026
1ece1b8
fix: close late review safety gaps
BigSimmo Jul 14, 2026
3ed3a7a
fix: keep generic table lookup within latency budget
BigSimmo Jul 14, 2026
a3f3a89
fix: bound agitation dose-route retrieval
BigSimmo Jul 14, 2026
dd79a95
fix: short-circuit adversarial retrieval
BigSimmo Jul 14, 2026
66f6d4a
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
9b56eeb
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
a2c4d52
fix: preserve agitation dose-route signals
BigSimmo Jul 14, 2026
ada86dd
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Jul 14, 2026
0792b17
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
578d10b
Merge remote-tracking branch 'origin/codex/release-blocker-remediatio…
BigSimmo Jul 14, 2026
1359de2
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
8a7ec72
fix: format clinical-search.ts for prettier CI check
cursoragent Jul 14, 2026
5b9302b
Merge remote-tracking branch 'origin/codex/release-blocker-remediatio…
BigSimmo Jul 14, 2026
ef448e2
Merge remote-tracking branch 'origin/main' into codex/release-blocker…
BigSimmo Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-14 | PR #655 / codex/release-blocker-remediation | 1ece1b891ed2d7f9577a060ae481e33cd4925ea4 + reviewed follow-up diff | offline release latency follow-up | The offline quality release gate reproducibly isolated one live timeout: a generic agitation table-lookup question was expanded into an unnecessary ten-term dose/route AND query, making lexical retrieval take about 39 seconds. Dose/route expansion now requires an actual dosing/route signal; the same case retrieves its expected source and four citations in 1.27 seconds without a model. | Focused clinical-search/retrieval Vitest 111/111; scoped ESLint; Prettier; `git diff --check`; live provider-free single-case quality eval passed with zero model/request/token/cost/generation evidence. |
| 2026-07-14 | PR #655 / codex/release-blocker-remediation | 3ed3a7a2df37d7d15143ab7606e5748ac7ecca09 + reviewed follow-up diff | offline dose-route latency follow-up | The next isolated timeout was a short IM/PO agitation question receiving the same blanket ten-term AND expansion. Agitation dose/route retrieval now keeps only the dose and route signals present in the question; the exact case retrieves its expected source and five citations in 1.54 seconds without a model. All remaining RAG cases 23–44 passed individually, so no further deadline crash remains. | Focused clinical-search/retrieval Vitest 111/111; scoped ESLint; Prettier; `git diff --check`; live provider-free case 22 passed; live provider-free cases 23–44 passed individually. |
| 2026-07-14 | PR #655 / codex/release-blocker-remediation | a3f3a89676015cd5f018c07e8c3ad9483f91cef6 + reviewed follow-up diff | offline adversarial-latency follow-up | The final blocking offline-quality failure was an adversarial secret-exfiltration query that correctly refused but first spent about 25 seconds in lexical retrieval. Adversarial manipulation now short-circuits at the search boundary before provider-client creation, cache access, classification, aliases, or Supabase work, and is never cached. | Focused Vitest 2/2; scoped ESLint; Prettier; full TypeScript; `git diff --check`; live provider-free adversarial case completed in 100 ms with 0 ms RPC time; `eval:quality:release:offline` passed with zero blocking failures and zero model, request-ID, token, cost, or generation-latency evidence. Flaky local browser/composite suites intentionally not repeated; hosted CI remains authoritative. |
| 2026-07-14 | PR #666 / codex/release-blocker-remediation | 9b56eebe4b23ab783207445fb827c317c8d59be8 + reviewed follow-up diff | review-followup | One late P2 retrieval-contract gap was confirmed: the optimized agitation query retained IM/PO but could drop other already-supported amount, route, and frequency aliases. Medication evidence intent is now shared with retrieval selection, and focused agitation queries preserve requested numeric units, SC, SL, PRN, and frequency signals without restoring the broad ten-term expansion. | GitHub review-thread inspection; focused clinical-search/retrieval Vitest 112/112; scoped ESLint; Prettier; `git diff --check`. Hosted final-head TypeScript/build/CI and exact-head staging evidence remain required after push. |
53 changes: 35 additions & 18 deletions src/lib/clinical-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,32 @@ export function medicationDoseEvidenceQueryIntent(query: string) {
};
}

/** Canonical text-search terms for the medication evidence attributes explicitly requested. */
export function medicationDoseEvidenceSearchTerms(query: string) {
const terms: string[] = [];
const intent = medicationDoseEvidenceQueryIntent(query);
if (intent.asksAmount) terms.push("dose");

for (const match of query.matchAll(
/\b(\d+(?:\.\d+)?)\s*(mg|mcg|micrograms?|milligrams?|ug)\b|\b(\d+(?:\.\d+)?)\s*([µμ]g)/gi,
Comment on lines +1208 to +1209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve hyphenated dose units

When users write doses as adjectival forms like 5-mg or 100-mcg (common in clinical copy), this regex does not match because it only allows whitespace between the number and unit. Since buildClinicalTextSearchQuery() replaces the token list with medicationDoseEvidenceSearchTerms(), those agitation queries collapse to agitation arousal dose and drop the explicit amount/unit instead of preserving 5 mg, weakening recall for dose-specific rows; a focused test such as buildClinicalTextSearchQuery("What 5-mg option is listed for agitation?") would catch it.

Useful? React with 👍 / 👎.

)) {
const amount = match[1] ?? match[3];
const unit = match[2] ?? match[4];
if (!amount || !unit) continue;
terms.push(amount, /^(?:mcg|microgram|ug|[µμ]g)/i.test(unit) ? "mcg" : "mg");
}

if (/\broutes?\b/i.test(query)) terms.push("route");
if (/\b(?:intramuscular|intramuscularly|im)\b/i.test(query)) terms.push("im");
if (/\b(?:oral|orally|po|by\s+mouth)\b/i.test(query)) terms.push("po");
if (/\b(?:subcutaneous|subcutaneously|subcut|sc)\b/i.test(query)) terms.push("sc");
if (/\b(?:sublingual|sublingually|sl)\b/i.test(query)) terms.push("sl");
if (/\bprn\b/i.test(query)) terms.push("prn");
else if (intent.asksFrequency) terms.push("frequency");

return [...new Set(terms.filter(Boolean))];
}

/** Whether the query explicitly asks for dose, route, or frequency evidence. */
export function isMedicationDoseEvidenceQuery(query: string) {
const intent = medicationDoseEvidenceQueryIntent(query);
Expand Down Expand Up @@ -1245,15 +1271,16 @@ export function buildClinicalTextSearchQuery(query: string) {
const hasAgitationArousalContext =
(hasAgitationArousalTypo && /\bagitation\b/i.test(correctedQueryText) && /\barousal\b/i.test(correctedQueryText)) ||
/\bagitation\b/i.test(correctedQueryText);
// Dose/route alias pattern aligned with buildRetrievalIntent in retrieval-selection.ts
const DOSE_ROUTE_ALIAS_PATTERN =
/\b(?:dose|doses|dosage|dosages|dosing|route|oral|intramuscular|subcutaneous|subcut|sublingual|im|po|sc|sl|frequency|mg|mcg|prn)\b/i;
const agitationMedicationIntent = medicationDoseEvidenceQueryIntent(query);
const wantsAgitationMedicationChart =
hasAgitationArousalContext &&
(agitationMedicationIntent.asksAmount ||
agitationMedicationIntent.asksRoute ||
agitationMedicationIntent.asksFrequency);
const wantsAgitationArousal =
hasAgitationArousalContext &&
/\b(?:dose|doses|dosage|dosages|dosing|guidance|inpatient|psychiatric|route|oral|intramuscular|subcutaneous|subcut|sublingual|\bim\b|\bpo\b|sc|sl|frequency|mg|mcg|prn|chart|table|pharmacolog)/i.test(
correctedQueryText,
);
const wantsAgitationMedicationChart = wantsAgitationArousal && DOSE_ROUTE_ALIAS_PATTERN.test(correctedQueryText);
(wantsAgitationMedicationChart ||
/\b(?:guidance|inpatient|psychiatric|chart|table|pharmacolog)/i.test(correctedQueryText));

if (wantsClozapineMissedDose) {
normalizedTokens.splice(0, normalizedTokens.length, "clozapine", "missed", "dose", "monitoring", "table");
Expand All @@ -1266,17 +1293,7 @@ export function buildClinicalTextSearchQuery(query: string) {
} else if (wantsClozapineBloodMonitoring) {
normalizedTokens.splice(0, normalizedTokens.length, "clozapine", "monitoring");
} else if (wantsAgitationMedicationChart) {
const requestedDoseRouteTerms = [
/\b(?:dose|doses|dosage|dosages|dosing)\b/i.test(correctedQueryText) ? "dose" : null,
/\broute\b/i.test(correctedQueryText) ? "route" : null,
/\b(?:intramuscular|im)\b/i.test(correctedQueryText) ? "im" : null,
/\b(?:oral|po)\b/i.test(correctedQueryText) ? "po" : null,
/\b(?:subcutaneous|subcut|sc)\b/i.test(correctedQueryText) ? "sc" : null,
/\b(?:sublingual|sl)\b/i.test(correctedQueryText) ? "sl" : null,
/\b(?:mg|mcg)\b/i.test(correctedQueryText) ? "mg" : null,
/\bfrequency\b/i.test(correctedQueryText) ? "frequency" : null,
/\bprn\b/i.test(correctedQueryText) ? "prn" : null,
].filter((term): term is string => Boolean(term));
const requestedDoseRouteTerms = medicationDoseEvidenceSearchTerms(query);
normalizedTokens.splice(0, normalizedTokens.length, "agitation", "arousal", ...requestedDoseRouteTerms);
} else if (wantsAgitationArousal) {
normalizedTokens.splice(0, normalizedTokens.length, "agitation", "arousal", "pharmacological", "management");
Expand Down
18 changes: 9 additions & 9 deletions src/lib/retrieval-selection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { citationFromResult, documentCitationHref } from "@/lib/citations";
import { medicationDoseQueryContext, medicationDoseQuerySubjectTokens } from "@/lib/clinical-search";
import {
medicationDoseEvidenceQueryIntent,
medicationDoseQueryContext,
medicationDoseQuerySubjectTokens,
} from "@/lib/clinical-search";
import type {
RagQueryClass,
RetrievalCandidate,
Expand Down Expand Up @@ -350,13 +354,10 @@ function resultBoost(args: { intent: RetrievalIntent; candidate: RetrievalCandid

export function buildRetrievalIntent(query: string, queryClass: RagQueryClass): RetrievalIntent {
const normalizedQuery = normalize(query);
const medicationEvidenceIntent = medicationDoseEvidenceQueryIntent(normalizedQuery);
const asksDoseRoute =
/\b(?:dose|doses|dosage|dosages|dosing|route|oral|intramuscular|subcutaneous|subcut|sublingual|im|po|sc|sl|frequency|mg|mcg|prn)\b/.test(
normalizedQuery,
);
const asksDoseAmount = /\b(?:dose|doses|dosage|dosages|dosing|mg|mcg|microgram|maximum|minimum)\b/.test(
normalizedQuery,
);
medicationEvidenceIntent.asksAmount || medicationEvidenceIntent.asksRoute || medicationEvidenceIntent.asksFrequency;
const asksDoseAmount = medicationEvidenceIntent.asksAmount;
const asksTable = /\b(?:table|chart|matrix|threshold|cutoff|cut off|range|criteria|row)\b/.test(normalizedQuery);
const asksSourceImage =
/\b(?:source|show|open|view|display|see)\b.*\b(?:image|figure|visual|table|chart|matrix)\b/.test(normalizedQuery) ||
Expand Down Expand Up @@ -398,8 +399,7 @@ export function buildRetrievalIntent(query: string, queryClass: RagQueryClass):
}
if (asksDoseRoute) {
if (asksDoseAmount) requiredTermSignals.push("dose_amount");
if (/\b(?:route|oral|intramuscular|subcutaneous|subcut|sublingual|im|po|sc|sl)\b/.test(normalizedQuery))
requiredTermSignals.push("route");
if (medicationEvidenceIntent.asksRoute) requiredTermSignals.push("route");
if (queryClass === "medication_dose_risk" && medicationDoseQuerySubjectTokens(query).length > 0) {
requiredTermSignals.push("clinical_subject");
}
Expand Down
15 changes: 15 additions & 0 deletions tests/clinical-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ describe("clinical search query normalization", () => {
).toBe("agitation arousal dose");
});

it("preserves supported agitation amount, route, and frequency signals without broad expansion", () => {
expect(buildClinicalTextSearchQuery("What 5 mg option is listed for agitation?")).toBe(
"agitation arousal dose 5 mg",
);
expect(buildClinicalTextSearchQuery("Which subcutaneous dose is listed for agitation?")).toBe(
"agitation arousal dose sc",
);
expect(buildClinicalTextSearchQuery("Which sublingual PRN option is listed for agitation?")).toBe(
"agitation arousal sl prn",
);
expect(buildClinicalTextSearchQuery("What dosing frequency is listed for agitation?")).toBe(
"agitation arousal dose frequency",
);
});

it("anchors clozapine blood-monitoring paraphrases to clozapine FBC evidence", () => {
expect(
buildClinicalTextSearchQuery(
Expand Down