From a553d65c4922987cfe498fd87561cc7fe1ead491 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:39:16 +0800 Subject: [PATCH 1/5] Improve typo tolerance across catalogue search --- docs/branch-review-ledger.md | 1 + src/components/factsheets/factsheets-data.ts | 7 +- src/components/therapy-compass/data/select.ts | 10 +++ src/lib/catalog-search.ts | 74 ++++++++++++++++++- src/lib/formulation.ts | 2 + src/lib/specifiers.ts | 3 + src/lib/therapies.ts | 2 + tests/catalog-search.test.ts | 18 ++++- tests/factsheets-data.test.ts | 1 + tests/formulation.test.ts | 4 + tests/specifiers.test.ts | 4 + tests/therapy-card-preview.test.ts | 31 +++++++- 12 files changed, 151 insertions(+), 6 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 171e63bcdb..59d3eff531 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -855,3 +855,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | PR #1782 / cursor/fix-document-open-scroll-e5bf | 5709f2cc7a954197e02107c96d7896d8d13445c3 | document-viewer open-at-top | ship: remove chunk mount scrollIntoView so document opens stay at overview top | document-viewer-shell.dom 7 pass; document-section-summary.dom 8 pass; verify:pr-local dry-run | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 98029875db7d640d3e699829249bb33892296bff | PR #1782 unblock | before: static-pr+coverage failed on stale adoption-manifest (document-viewer-shell testFiles drift), merge-tree clean 0 behind, auto-merge armed, 1 advisory CodeRabbit waitFor thread; after: regenerated adoption-manifest, hardened scroll negative assertion, pre-commit+handoff adoption sync to prevent recurrence; CodeRabbit dispositioned as fixed by sync assert | check:design-system-adoption PASS; vitest design-system-adoption+document-viewer-shell+docs-inventory 63/63 PASS; format; no provider-backed checks | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 86698228533ebe10452c10c1bd7a3e1610d891ae | PR #1782 unblock | merged origin/main (behind-but-clean); fixed static-pr TS2322 on document-viewer-shell chunk fixture; fixed Production UI DSM compare remove stall via location.assign + DOM proof; prior adoption-manifest drift already fixed | tsc clean for changed files; vitest document-viewer-shell+dsm-compare-remove+design-system-adoption 59/59 PASS; check:design-system-adoption PASS; format; no provider-backed checks | +| 2026-08-10 | work | d812c7692505e9f68e7c428d26c67e53ef001046 | non-answer mode search typo tolerance and closest-match behavior | P2 exact-text-only catalogue matching fixed with conservative typo tolerance; answer RAG unchanged | 5 focused Vitest files (49 tests), typecheck, changed-file ESLint | diff --git a/src/components/factsheets/factsheets-data.ts b/src/components/factsheets/factsheets-data.ts index a78b46fea7..1f925c5d92 100644 --- a/src/components/factsheets/factsheets-data.ts +++ b/src/components/factsheets/factsheets-data.ts @@ -1,3 +1,5 @@ +import { fuzzySearchTokenCount } from "@/lib/catalog-search"; + /** * Patient factsheet library — content model and helpers. * @@ -659,9 +661,8 @@ export function filterFactsheets(query: string, category?: string): Factsheet[] if (!q) return true; // Include the brand suffix (e.g. "(Zoloft)") so brand-name searches resolve // even though it is stored separately from the title. - return `${sheet.title} ${sheet.brand ?? ""} ${sheet.summary} ${sheet.category} ${sheet.audience}` - .toLowerCase() - .includes(q); + const text = `${sheet.title} ${sheet.brand ?? ""} ${sheet.summary} ${sheet.category} ${sheet.audience}`; + return text.toLowerCase().includes(q) || fuzzySearchTokenCount(q, text) > 0; }); } diff --git a/src/components/therapy-compass/data/select.ts b/src/components/therapy-compass/data/select.ts index cc60850dde..2650a0fc4f 100644 --- a/src/components/therapy-compass/data/select.ts +++ b/src/components/therapy-compass/data/select.ts @@ -1,4 +1,5 @@ import type { Therapy } from "./types"; +import { fuzzySearchTokenCount } from "@/lib/catalog-search"; // ---- text helpers ------------------------------------------------------- @@ -153,6 +154,15 @@ function scoreTherapy(t: Therapy, q: string): number { if (lc(t.targetSymptoms).includes(q)) score += 5; if (lc(t.clinicalSummary).includes(q)) score += 3; if (lc(t.indications).includes(q)) score += 3; + if (score === 0) { + score += + fuzzySearchTokenCount( + q, + [t.name, ...t.aliases, ...t.tags, t.category, t.bestUsedFor, t.targetSymptoms, t.clinicalSummary, t.indications] + .filter(Boolean) + .join(" "), + ) * 2; + } return score; } diff --git a/src/lib/catalog-search.ts b/src/lib/catalog-search.ts index b06a1daa1c..df3e3ae472 100644 --- a/src/lib/catalog-search.ts +++ b/src/lib/catalog-search.ts @@ -24,6 +24,63 @@ export function compactSearchText(value: string) { return value.replace(/\s+/g, ""); } +function typoDistanceLimit(term: string) { + if (term.length >= 8) return 2; + if (term.length >= 4) return 1; + return 0; +} + +/** + * Bounded Damerau-Levenshtein distance for catalogue search. The short-token + * guard prevents clinically meaningful abbreviations (for example, MDD/GAD) + * from being broadened, while the transposition case catches common typing + * errors without involving document retrieval or answer-mode RAG. + */ +function boundedTypoDistance(left: string, right: string, limit: number) { + if (Math.abs(left.length - right.length) > limit) return limit + 1; + const previous = Array.from({ length: right.length + 1 }, (_, index) => index); + let previousPrevious: number[] | undefined; + + for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) { + const current = [leftIndex]; + let rowMinimum = current[0]; + for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) { + const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1; + let distance = Math.min( + current[rightIndex - 1] + 1, + previous[rightIndex] + 1, + previous[rightIndex - 1] + substitutionCost, + ); + if ( + previousPrevious && + leftIndex > 1 && + rightIndex > 1 && + left[leftIndex - 1] === right[rightIndex - 2] && + left[leftIndex - 2] === right[rightIndex - 1] + ) { + distance = Math.min(distance, previousPrevious[rightIndex - 2] + 1); + } + current[rightIndex] = distance; + rowMinimum = Math.min(rowMinimum, distance); + } + if (rowMinimum > limit) return limit + 1; + previousPrevious = previous.slice(); + previous.splice(0, previous.length, ...current); + } + return previous[right.length]; +} + +/** Number of query tokens with a conservative near-word match in normalized text. */ +export function fuzzySearchTokenCount(query: string, text: string) { + const queryTokens = normalizeSearchText(query).split(/\s+/).filter(Boolean); + const words = Array.from(new Set(normalizeSearchText(text).split(/\s+/).filter(Boolean))); + return queryTokens.filter((term) => { + if (words.some((word) => word.includes(term))) return false; + const limit = typoDistanceLimit(term); + return limit > 0 && words.some((word) => boundedTypoDistance(term, word, limit) <= limit); + }).length; +} + export type CatalogField = { // Wrapper-facing key used to build human-readable reasons (e.g. "title", "contact"). id: string; @@ -39,6 +96,7 @@ export type CatalogMatchSignals = { // Matched term count for terms introduced by expandTokens (e.g. symptom // aliases) that were not part of the raw query. expanded: number; + fuzzy: number; compact: boolean; phrase: boolean; prefix: boolean; @@ -130,6 +188,10 @@ export function rankCatalogRecords( score += content * contentWeight; const expanded = expandedTerms.filter((term) => text.includes(term)).length; + const fuzzy = score === 0 ? fuzzySearchTokenCount(normalizedQuery, text) : 0; + // Fuzzy evidence is deliberately weaker than a literal content hit. It + // rescues misspellings but cannot outrank a correctly matched title. + score += fuzzy * Math.max(1, contentWeight * 0.5); const compact = compactBonus > 0 && @@ -158,7 +220,17 @@ export function rankCatalogRecords( record, index, score, - signals: { fields, content, expanded, compact, phrase, prefix, exact, broad } satisfies CatalogMatchSignals, + signals: { + fields, + content, + expanded, + fuzzy, + compact, + phrase, + prefix, + exact, + broad, + } satisfies CatalogMatchSignals, }; }) .filter((match) => match.score > 0) diff --git a/src/lib/formulation.ts b/src/lib/formulation.ts index a7a83ed8b2..46ded32b3a 100644 --- a/src/lib/formulation.ts +++ b/src/lib/formulation.ts @@ -1,4 +1,5 @@ import formulationContentJson from "@/data/formulation-content.json"; +import { fuzzySearchTokenCount } from "@/lib/catalog-search"; export type FormulationMechanism = { id: string; @@ -228,6 +229,7 @@ export function searchFormulationMechanisms(query: string, options: { domain?: s if (clues.includes(token)) score += 8; if (haystack.includes(token)) score += 3; } + if (score === 0) score += fuzzySearchTokenCount(normalizedQuery, haystack) * 2; } return score > 0 ? { mechanism, score } : null; diff --git a/src/lib/specifiers.ts b/src/lib/specifiers.ts index a9bfc0c7e9..5c2441610b 100644 --- a/src/lib/specifiers.ts +++ b/src/lib/specifiers.ts @@ -1,3 +1,5 @@ +import { fuzzySearchTokenCount } from "@/lib/catalog-search"; + export type SpecifierFamily = "episode-features" | "course-onset" | "severity-remission"; export type SpecifierBuilderDiagnosis = @@ -712,6 +714,7 @@ export function searchSpecifiers( if (keywords.includes(token)) score += 10; if (haystack.includes(token)) score += 3; } + if (normalizedQuery && score === 0) score += fuzzySearchTokenCount(normalizedQuery, haystack) * 2; return { record, score }; }) diff --git a/src/lib/therapies.ts b/src/lib/therapies.ts index 61d42e4472..ce7ced8a1e 100644 --- a/src/lib/therapies.ts +++ b/src/lib/therapies.ts @@ -1,4 +1,5 @@ import therapiesIndexJson from "@/data/therapies-index.json"; +import { fuzzySearchTokenCount } from "@/lib/catalog-search"; // Server-side therapy catalogue. Backed by src/data/therapies-index.json — a trimmed, // rankable projection of the ~2.5 MB public Therapy Compass dataset (regenerated by @@ -119,6 +120,7 @@ export function searchTherapyRecords(query: string): TherapySearchMatch[] { if (tags.includes(token)) score += 6; if (haystack.includes(token)) score += 3; } + if (score === 0) score += fuzzySearchTokenCount(normalizedQuery, haystack) * 2; } return score > 0 ? { record, score } : null; diff --git a/tests/catalog-search.test.ts b/tests/catalog-search.test.ts index c788eff509..8082f5fd47 100644 --- a/tests/catalog-search.test.ts +++ b/tests/catalog-search.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { compactSearchText, normalizeSearchText, rankCatalogRecords } from "../src/lib/catalog-search"; +import { + compactSearchText, + fuzzySearchTokenCount, + normalizeSearchText, + rankCatalogRecords, +} from "../src/lib/catalog-search"; type Item = { title: string; slug: string; tags: string[]; body: string }; @@ -56,6 +61,17 @@ describe("rankCatalogRecords", () => { expect(results.some((match) => match.record.slug === "lithium-levels")).toBe(false); }); + it("finds close catalogue words after an insertion, omission, or transposition", () => { + expect(rank("clozpaine")[0]?.record.slug).toBe("clozapine-monitoring"); + expect(rank("lithum")[0]?.record.slug).toBe("lithium-levels"); + expect(fuzzySearchTokenCount("monitroing", "Clozapine monitoring guidance")).toBe(1); + }); + + it("does not fuzz short clinical abbreviations or unrelated words", () => { + expect(fuzzySearchTokenCount("GAD", "Major depressive disorder")).toBe(0); + expect(rank("transport").some((match) => match.record.slug === "clozapine-monitoring")).toBe(false); + }); + it("applies the whole-phrase bonus on top of term matches", () => { const [top] = rank("clozapine monitoring"); // 2 title terms (12) + 2 content terms (4) + phrase (4). diff --git a/tests/factsheets-data.test.ts b/tests/factsheets-data.test.ts index 040421e1a1..645604d3a7 100644 --- a/tests/factsheets-data.test.ts +++ b/tests/factsheets-data.test.ts @@ -69,6 +69,7 @@ describe("factsheet library", () => { expect(filterFactsheets("sertraline").map((sheet) => sheet.slug)).toContain("sertraline"); // Brand suffix ("(Zoloft)") is indexed even though it is stored separately from the title. expect(filterFactsheets("Zoloft").map((sheet) => sheet.slug)).toContain("sertraline"); + expect(filterFactsheets("sertralne").map((sheet) => sheet.slug)).toContain("sertraline"); const conditions = filterFactsheets("", "Conditions"); expect(conditions.length).toBeGreaterThan(0); expect(conditions.every((sheet) => sheet.category === "Conditions")).toBe(true); diff --git a/tests/formulation.test.ts b/tests/formulation.test.ts index 5e17a451ec..0ecfc791ee 100644 --- a/tests/formulation.test.ts +++ b/tests/formulation.test.ts @@ -52,6 +52,10 @@ describe("clinical formulation content", () => { expect(searchFormulationMechanisms("If it is not perfect it is a failure")[0]?.mechanism.id).toBe("perfectionism"); }); + it("recovers a close mechanism-name typo", () => { + expect(searchFormulationMechanisms("rumiantion")[0]?.mechanism.id).toBe("rumination"); + }); + it("filters the mechanism catalogue by formulation domain", () => { const trauma = searchFormulationMechanisms("", { domain: "Trauma" }); expect(trauma.length).toBeGreaterThan(0); diff --git a/tests/specifiers.test.ts b/tests/specifiers.test.ts index eaee2fac73..dcc5c75fa2 100644 --- a/tests/specifiers.test.ts +++ b/tests/specifiers.test.ts @@ -36,6 +36,10 @@ describe("psychiatric specifier catalogue", () => { expect(searchSpecifiers("much better but not fully recovered")[0]?.record.slug).toBe("in-partial-remission"); }); + it("recovers a close specifier typo", () => { + expect(searchSpecifiers("melancholc")[0]?.record.slug).toBe("with-melancholic-features"); + }); + it("filters by diagnostic role and diagnosis context", () => { const courseResults = searchSpecifiers("", { family: "course-onset" }); expect(courseResults.length).toBeGreaterThan(0); diff --git a/tests/therapy-card-preview.test.ts b/tests/therapy-card-preview.test.ts index 54a3b36f23..5adb482242 100644 --- a/tests/therapy-card-preview.test.ts +++ b/tests/therapy-card-preview.test.ts @@ -1,6 +1,35 @@ import { describe, expect, it } from "vitest"; -import { cardPreviewText, prioritiseTherapyTags } from "@/components/therapy-compass/data/select"; +import { cardPreviewText, prioritiseTherapyTags, searchTherapies } from "@/components/therapy-compass/data/select"; +import type { Therapy } from "@/components/therapy-compass/data/types"; + +const searchableTherapy = { + slug: "behavioural-activation", + name: "Behavioural activation", + aliases: [], + tags: ["depression"], + category: "Behavioural", + bestUsedFor: "Low mood", + targetSymptoms: "withdrawal", + clinicalSummary: "A structured activity-based therapy.", + indications: "Depression", + briefInterventionAvailable: false, + patientSheetAvailable: false, + reviewStatus: "reviewed", +} as unknown as Therapy; + +describe("searchTherapies", () => { + it("recovers a close therapy-name typo", () => { + const results = searchTherapies([searchableTherapy], { + query: "behavoural activaton", + tags: [], + briefOnly: false, + sheetOnly: false, + reviewedOnly: false, + }); + expect(results[0]?.slug).toBe("behavioural-activation"); + }); +}); describe("cardPreviewText", () => { it("skips a leading sentence that restates the therapy name", () => { From 27a7d72f1b0e4ae5a3f818bce76e0bd38c89093d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 12:59:42 +0000 Subject: [PATCH 2/5] Exclude four-char clinical abbreviations from fuzzy catalogue search SSRI and SNRI are one Damerau-Levenshtein edit apart; raise the fuzzy floor to five characters and pin a regression so medication-class labels do not cross-match in catalogue fallback consumers. --- docs/branch-review-ledger.md | 1 + src/lib/catalog-search.ts | 11 +++++++---- tests/catalog-search.test.ts | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 8a8a1a48a5..e4ffb7f0e8 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -878,3 +878,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | a4f57500f6b16a4616e1f84c126c2f787a40766b | PR #1785 unblock/fix | before: DIRTY/CONFLICTING behind-but-clean (merge-tree clean, behind 3/ahead 8); prior tip a4f57500 CI green; 0 unresolved threads → after: merged origin/main once (sync-only); merge-tree clean; behind 0; no CI/thread code fixes; focused meds tests 201 passed | git merge-tree clean; npm run format; npm run test:focused meds/route/universal-search 201 passed; no provider-backed checks run | | 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 88dbdd80ede81ec6062ffebae79244703d495a99 | PR #1785 unblock/fix | before: BEHIND/MERGEABLE behind-but-clean (merge-tree clean, behind 2/ahead 9); tip 88dbdd80 required CI green; → after: late merged origin/main once (#1793/#1794); merge-tree clean; behind 0; no required-CI code fixes; no provider-backed checks | git merge-tree clean; npm run format; prior tip CI green; no provider-backed checks run | | 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 5cb0e11e077a3aaf5b8e4ea37b26ac72b0328997 | PR #1785 unblock/fix | before: Production UI (3) failed on service-detail scroll endpoint (remaining 67px) at 38b3bd0c; GitHub DIRTY behind-but-clean vs #1791. after: merged origin/main + re-scroll toPass fix in ui-tools service-detail test; threads untouched; do not merge | CI Production UI (3) logs; git merge-tree clean; prettier ui-tools; product fix in same tip commit as this row | +| 2026-08-10 | PR #1800 / codex/enhance-search-function-with-fuzzy-matching | 93da84b063c9c3f956da7ef79710d2cd00159735 | PR #1800 babysit | Synced origin/main (merge-tree clean; GitHub DIRTY was staleness). Fixed CodeRabbit SSRI/SNRI fuzzy cross-match (floor 5 chars) in follow-on tip commit. Clinical Governance Preflight required for clinicalRisk body. Codex P2 field-aware/per-token fuzzy deferred. RAG surfaces untouched. | focused catalog-search+consumers 49 pass; pr-policy body local ok; merge-tree clean | diff --git a/src/lib/catalog-search.ts b/src/lib/catalog-search.ts index df3e3ae472..30d18627f0 100644 --- a/src/lib/catalog-search.ts +++ b/src/lib/catalog-search.ts @@ -26,15 +26,18 @@ export function compactSearchText(value: string) { function typoDistanceLimit(term: string) { if (term.length >= 8) return 2; - if (term.length >= 4) return 1; + // Four-character clinical abbreviations (SSRI/SNRI, ADHD/ODD, etc.) are + // often one edit apart; require five characters before allowing a typo. + if (term.length >= 5) return 1; return 0; } /** * Bounded Damerau-Levenshtein distance for catalogue search. The short-token - * guard prevents clinically meaningful abbreviations (for example, MDD/GAD) - * from being broadened, while the transposition case catches common typing - * errors without involving document retrieval or answer-mode RAG. + * guard prevents clinically meaningful abbreviations (for example, MDD/GAD + * and four-character medication-class labels such as SSRI/SNRI) from being + * broadened, while the transposition case catches common typing errors + * without involving document retrieval or answer-mode RAG. */ function boundedTypoDistance(left: string, right: string, limit: number) { if (Math.abs(left.length - right.length) > limit) return limit + 1; diff --git a/tests/catalog-search.test.ts b/tests/catalog-search.test.ts index 8082f5fd47..d67f4e873d 100644 --- a/tests/catalog-search.test.ts +++ b/tests/catalog-search.test.ts @@ -69,6 +69,9 @@ describe("rankCatalogRecords", () => { it("does not fuzz short clinical abbreviations or unrelated words", () => { expect(fuzzySearchTokenCount("GAD", "Major depressive disorder")).toBe(0); + // Four-character medication-class abbreviations are one edit apart and + // must not fuzzy-match each other (CodeRabbit on PR #1800). + expect(fuzzySearchTokenCount("SSRI", "SNRI")).toBe(0); expect(rank("transport").some((match) => match.record.slug === "clozapine-monitoring")).toBe(false); }); From c2f08183668a445374f42b0adf65ed01fd59c8ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 13:01:17 +0000 Subject: [PATCH 3/5] Add PR_POLICY_BODY.md so CI can sync Clinical Governance preflight Cloud agent tokens cannot PATCH pull request bodies; the Sync PR policy body job applies this scratch template with pull-requests:write. Remove before merge so it does not land on main (#230). --- PR_POLICY_BODY.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 PR_POLICY_BODY.md diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md new file mode 100644 index 0000000000..8d95d40455 --- /dev/null +++ b/PR_POLICY_BODY.md @@ -0,0 +1,34 @@ +## Summary + +- Make non-answer catalogue/local-library searches resilient to common typos and transpositions so results surface without exact-string matches. +- Keep answer/RAG document retrieval unchanged by restricting fuzzy matching to catalogue/ranker code as a low-priority fallback. +- Bound fuzzy logic by token length (0 edits below 5 characters, 1 edit for 5–7, 2 edits for >=8) so short clinical abbreviations such as SSRI/SNRI do not cross-match. + +## Verification + +- [x] Focused Vitest: `node scripts/run-vitest.mjs run tests/catalog-search.test.ts tests/specifiers.test.ts tests/formulation.test.ts tests/factsheets-data.test.ts tests/therapy-card-preview.test.ts --reporter=dot` — 49 passed +- [x] `npm run typecheck` and changed-file ESLint checks passed for the edited files +- [x] `npm run format` completed cleanly +- UI verification not run: no UI component markup or routing changes; search behaviour covered by unit tests above +- Verification not run: `npm run verify:pr-local` full gate deferred to hosted CI after branch sync + +## Risk and rollout + +- Risk: Low–moderate clinical-search UX risk — typo fallback can surface near matches in catalogue modes; four-character clinical abbreviations are excluded from fuzzy matching. +- Rollback: Revert the PR commit(s); catalogue search returns to exact/substring matching only. +- Provider or production effects: None — offline catalogue matching only; no OpenAI/Supabase/provider calls added. + +## Clinical Governance Preflight + +- [x] Source-backed claims still require linked source verification before clinical use +- [x] No patient-identifiable document workflow was introduced or expanded without explicit governance approval +- [x] Supabase target remains `Clinical KB Database` (`sjrfecxgysukkwxsowpy`) +- [x] Service-role keys and private document access remain server-only +- [x] Demo/synthetic content remains clearly separated from real clinical sources +- [x] Source metadata, review status, and outdated/unknown-source behavior remain conservative +- [x] Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed + +## Notes + +- Original Codex task: https://chatgpt.com/codex/cloud/tasks/task_e_6a79a1a17fc08322bd3c06f3af045cff +- Babysit: synced `origin/main` (merge-tree was clean; GitHub DIRTY was staleness) and raised the fuzzy floor to five characters for the SSRI/SNRI CodeRabbit finding. From 2fbbe1848fe31165692661918382a511ff4e30ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 13:04:06 +0000 Subject: [PATCH 4/5] Remove PR_POLICY_BODY.md after CI synced the PR description Keeps the scratch template off the merge tip (#230) and retriggers PR policy against the already-synced Clinical Governance Preflight body. --- PR_POLICY_BODY.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 PR_POLICY_BODY.md diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md deleted file mode 100644 index 8d95d40455..0000000000 --- a/PR_POLICY_BODY.md +++ /dev/null @@ -1,34 +0,0 @@ -## Summary - -- Make non-answer catalogue/local-library searches resilient to common typos and transpositions so results surface without exact-string matches. -- Keep answer/RAG document retrieval unchanged by restricting fuzzy matching to catalogue/ranker code as a low-priority fallback. -- Bound fuzzy logic by token length (0 edits below 5 characters, 1 edit for 5–7, 2 edits for >=8) so short clinical abbreviations such as SSRI/SNRI do not cross-match. - -## Verification - -- [x] Focused Vitest: `node scripts/run-vitest.mjs run tests/catalog-search.test.ts tests/specifiers.test.ts tests/formulation.test.ts tests/factsheets-data.test.ts tests/therapy-card-preview.test.ts --reporter=dot` — 49 passed -- [x] `npm run typecheck` and changed-file ESLint checks passed for the edited files -- [x] `npm run format` completed cleanly -- UI verification not run: no UI component markup or routing changes; search behaviour covered by unit tests above -- Verification not run: `npm run verify:pr-local` full gate deferred to hosted CI after branch sync - -## Risk and rollout - -- Risk: Low–moderate clinical-search UX risk — typo fallback can surface near matches in catalogue modes; four-character clinical abbreviations are excluded from fuzzy matching. -- Rollback: Revert the PR commit(s); catalogue search returns to exact/substring matching only. -- Provider or production effects: None — offline catalogue matching only; no OpenAI/Supabase/provider calls added. - -## Clinical Governance Preflight - -- [x] Source-backed claims still require linked source verification before clinical use -- [x] No patient-identifiable document workflow was introduced or expanded without explicit governance approval -- [x] Supabase target remains `Clinical KB Database` (`sjrfecxgysukkwxsowpy`) -- [x] Service-role keys and private document access remain server-only -- [x] Demo/synthetic content remains clearly separated from real clinical sources -- [x] Source metadata, review status, and outdated/unknown-source behavior remain conservative -- [x] Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed - -## Notes - -- Original Codex task: https://chatgpt.com/codex/cloud/tasks/task_e_6a79a1a17fc08322bd3c06f3af045cff -- Babysit: synced `origin/main` (merge-tree was clean; GitHub DIRTY was staleness) and raised the fuzzy floor to five characters for the SSRI/SNRI CodeRabbit finding. From 87cd5b62913657a72a8326ecafbf342ddbbd58b3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 13:15:52 +0000 Subject: [PATCH 5/5] fix(test): select formulation framework via visible label Production UI shard 1 failed locator.check({ force }) on the controlled sr-only CBT cycle radio. Click the visible label instead. --- tests/ui-formulation.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/ui-formulation.spec.ts b/tests/ui-formulation.spec.ts index 27bd492944..82657f831a 100644 --- a/tests/ui-formulation.spec.ts +++ b/tests/ui-formulation.spec.ts @@ -202,12 +202,10 @@ test("moves a selected mechanism through framework, quality review, and an edita await expect(page.getByTestId("formulation-builder-structure")).toBeVisible(); const frameworkGroup = page.getByRole("radiogroup", { name: "Formulation framework" }); const cbtCycle = frameworkGroup.getByRole("radio", { name: /CBT cycle/ }); - // Input is `sr-only` (not actionable for Playwright hit-testing). Prefer - // role-based check with force so activation does not depend on scroll-into-view - // of a clipped control (Production UI shard 1 failure on PR #1788). Keep the - // radiogroup scope so shard contention with ui-specifiers cannot hit a stray - // "CBT cycle" text node (#257). - await cbtCycle.check({ force: true }); + // Controlled `sr-only` radios still flake under `locator.check({ force })` on + // Production UI shard 1 (state does not flip). Drive selection through the + // visible label click handler instead, then assert the radio role. + await frameworkGroup.getByText("CBT cycle", { exact: true }).click(); await expect(cbtCycle).toBeChecked(); await page .getByRole("textbox", { name: "Presenting problem" })