diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 07cf352f28..ef236e67a3 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -840,7 +840,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | claude/document-viewer-phase-3-bj5k5v | 156db63f1b60f09791e426b043ea90d427b789ab | post-#1772 test simplification: replace the viewer perf source-text grep with behavioural coverage; de-literalise rail window and keyboard label assertions | PR #1777 opened. Self-review of #1772's own tests against an excessive-strictness challenge. Finding: the client-performance-boundaries grep for resolveLiveCanvasWindow / resolveRenderAheadPages / liveCanvasLimit / requestIdleCallback was not merely brittle, it was INEFFECTIVE - replacing the budget call with a hardcoded 3 leaves every identifier in the file, so it stayed green while the viewer retained three full-zoom canvases (measured both ways). Replaced by a DOM case that binds the budget (VIEWER_MAX_ZOOM at dpr 3 gives ~16.8M backing px against the 24M budget, window collapses to 1) and fails on exactly that substitution. Also exported RAIL_IMAGE_WINDOW so the rail test derives its counts (verified by tuning 6->8: all 7 still pass), and relaxed the keyboard aria-label assertions from exact prose to the key names. Pre-existing greps for disableAutoFetch / canvas.width = 0 / pageToCleanup left alone deliberately - two are now redundant but they are another author's guard. | verify:pr-local (1 pre-existing root-only failure: pr-handoff-stop #291; 5872 passed), build OK 80s + client bundle secret check, eval:rag:offline 36 golden cases / 574 tests, lint + typecheck clean. Sabotage-verified in both directions. Browser gates unrunnable here (#279) - unchanged by this diff. | | 2026-08-09 | claude/planning-build-intelligence-9ot0nm | 3df3cb3993f73cda4dbbc4ac7549f84b3c6ea7ed | Node 24.15 engine floor: engines.node, preinstall hook, check:runtime, session-start provisioning, codex-cloud assertion | Authored and handed off as PR #1771; closes #285; operationalRisk true, clinicalRisk/ragRanking false | test 5800 passed/1 pre-existing root-uid failure (pr-handoff-stop, confirmed on stashed clean tree); lint 0; typecheck 0; prettier --check . pass; check:runtime pass; check:codex-cloud pass; check:outstanding-issues pass; preinstall boundary proof 24.13/24.14.9 reject, 24.15/24.19 accept, 25.0.0 reject; contract test mutation-checked red | | 2026-08-09 | pull/1771 | 466ec4216272c31c5f754db213dbdc529583b167 | PR 1771 runtime floor enforcement | P2: Cloud and Desktop setup paths remain major-only; do not merge until range-aware | static review; check:runtime PASS; check:codex-cloud PASS; ledger PASS; outstanding issues PASS; focused Vitest blocked by active Playwright lease | -| 2026-08-09 | cursor/smarter-meds-search-9c1b | 74c3ea7706802925040b2c5603a7140a54dc9cd3 | medications-catalog-search typos brands | shipped catalog-local typo/brand search; no RAG | npm run test: 5899 passed | +| 2026-08-09 | cursor/therapy-card-densify-e975 | 3db839a6bb1f5b45fc55bb732d21b30551a506b0 | therapy search ResultCard densify (gap, tags, favourite, actions, match cells) | pass — denser cards; band gap fixed; single-row prioritized tags; heart top-right; 3-col actions; summarised cells | unit 35/35; verify:pr-local pass; ensure visual phone+desktop pass | | 2026-08-09 | claude/m3-token-debt-262-261 | c6e1fe7fc42ec6f286eb5a3d8f7ddad7dfad2724 | design-system contract: raw padding/radius/line-height ratchets + type-step selection gate (#262 parts 2/3); closed #218/#270 | Authored and self-verified; PR #1780 open, auto-merge deliberately not armed (gate change). Baseline additive: all 15 pre-existing metrics and every debtByPath entry byte-identical; 94/94 new findings verified present at their cited line. Mutation-tested both halves of part 3 and three failure modes of part 2. | check:design-system-contract, check:icon-scale, check:type-scale, check:outstanding-issues, vitest design-system-contract-utils (31 passed), format:check whole-tree, verify:cheap (exit 1 from 5 pre-existing failures, none in this diff; 3 cleared by merging main, remaining 2 byte-identical to origin/main) | | 2026-08-09 | claude/documentviewer-nav-convergence-oddhjx | 1395d533cb13eadc705e47f76aa9f39a7a11c058 | DocumentViewer / in-page-nav convergence (#288): non-adoption decision recorded in docs/search-chrome-behaviour.md; merged duplicated visible-element predicate into resolveVisibleElement; new convergence guard test | Converged what was duplicated; DocumentReviewer header adoption declined on the merits with four blocking reasons recorded. No contract test edited. | verify:pr-local (546/547 files, 5883 tests pass; sole failure tests/pr-handoff-stop.test.ts reproduced on pristine origin/main), verify:phone-chrome (contracts 123 pass; focused Chromium 7 pass), contract set 12 files/151 tests pass, lint, typecheck, format | | 2026-08-09 | claude/disabled-button-accessibility-piclvr | 722abdb780c715c0a89df268ed48f6c741ffd569 | disabled-placeholder buttons -> aria-disabled + inert handler (25 sites, 13 components); controlDisabled/therapy recipe aria-disabled styling; require-button-wiring redundantDisabledPair gate; wiring-conventions contract rewrite (settles #291) | authored — PR #1778 opened | lint (uncached, exit 0); typecheck; test 5878 passed/1 pre-existing root-env failure in pr-handoff-stop; build; check:rag:fixtures 36 golden cases; prettier --check clean; verify:ui not run (no browser in container) | @@ -850,20 +850,17 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | claude/m3-token-debt-262-261 | 7bac3bd762b381cb25c9b2a15ef3bb7223d15b16 | PR #1780 review-and-fix | fixed P2 ratchet bypasses (arbitrary-property classes, CSS-consumer exemption anti-rot, modern CSS zero units); Bugbot clean; merge-tree clean; required CI was green on prior tip | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption fail→restore; verify:cheap PASS (549 files / 5933 tests); verify:pr-local stages PASS (test flake in design-system-adoption timed out once then 51/51 + full test 549/549 + check:rag:fixtures PASS); no provider gates | | 2026-08-09 | cursor/dsm-search-header-fix-15d6 | df088c766f1761496189ec09146aa54c23b1c012 | dsm-search-header | pass: removed catalogue page strip; ribbon + category filter match target | vitest dsm-search-empty-state; npm test 5857 passed; lint; typecheck; ensure phone /dsm/search?q=Delirium | | 2026-08-09 | claude/m3-token-debt-262-261 | fe75e6acade008e68f953e235cc035f2e5d9d216 | PR #1780 review-and-fix | fixed P2 ratchet bypasses; synced origin/main (#1775); Bugbot clean; merge-tree clean | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption; verify:cheap PASS 549/5933; verify:pr-local stages PASS after adoption flake retest; check:rag:fixtures PASS; no provider gates | -| 2026-08-09 | cursor/smarter-meds-search-9c1b | aced65e055892b0e2927b3999f95c6435102f610 | medications-catalog-search typos brands | main sync; catalog-local typo/brand search complete; no RAG | medications+route tests 49 passed; merge-tree clean | +| 2026-08-09 | cursor/therapy-card-densify-e975 | 52f07d49f89e6c786c624ccbd38ae552818a2071 | PR 1783 babysit | fixed review threads: TagRow +N clip, title/alias preview exclusion, preview field fallbacks; Copilot md grid kept; CI re-triggered after Copilot tip | npm test: 5958 passed / 4 skipped | | 2026-08-09 | claude/in-page-nav-pr-3-i6gi8n | 6651feef4fab63f1181fba57908cb22e2932df3c | in-page-nav PR 3: convert /medications/[slug] (panel-swap) and /factsheets/[slug] (anchors) onto InPageNavHeader; record the differentials-presentations exception; delete orphaned SecondaryNavigation (#271) | converted 2 of 3 routes, 3rd recorded as a reasoned lasting exception; tocFor and SecondaryNavigation deleted; route-sections contract 7 -> 12 routes plus a panel-swap suite | verify:pr-local (1 pre-existing root-permission failure in pr-handoff-stop.test.ts, all else green); test 5932 passed; in-page-nav-route-sections 29 passed; verify:phone-chrome 3/4 stages (focused-browser blocked by #255 Chromium 1194 vs 1234); build + bundle-budget + rag:fixtures green; verify:ui not run (#255, delegated to CI) | -| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | 94dd432c0f64fd0593ea68d15aaa612240e7cd1d | PR #1785 unblock/fix | synced origin/main (behind-but-clean); merge-tree clean; prior tip CI green except PR mergeability DIRTY; review threads already cleared | merge-tree clean; behind 0; prior abb827b1 PR required+Production UI green; focused meds tests to re-run after sync | | 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-09 | cursor/smarter-meds-search-9c1b (PR #1785) | b722c628ca05eb32190ac6355e8ee0537817621c | PR #1785 unblock/fix | synced origin/main (#1782); behind-but-clean DIRTY cleared; merge-tree clean; review threads clear; prior tip product CI green | merge-tree clean; behind 0; test:focused meds after sync | -| 2026-08-09 | cursor/therapy-card-densify-e975 | 3db839a6bb1f5b45fc55bb732d21b30551a506b0 | therapy search ResultCard densify (gap, tags, favourite, actions, match cells) | pass — denser cards; band gap fixed; single-row prioritized tags; heart top-right; 3-col actions; summarised cells | unit 35/35; verify:pr-local pass; ensure visual phone+desktop pass | -| 2026-08-09 | cursor/therapy-card-densify-e975 | 52f07d49f89e6c786c624ccbd38ae552818a2071 | PR 1783 babysit | fixed review threads: TagRow +N clip, title/alias preview exclusion, preview field fallbacks; Copilot md grid kept; CI re-triggered after Copilot tip | npm test: 5958 passed / 4 skipped | -| 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 | 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 | +| 2026-08-10 | PR #1788 | cc64d8356ca964ef2e550f4493f17778965ada79 | Run PR sweep | merged origin/main (resolved in-page-nav ContextualBackLink+rail); fixed restore metadata/signature/guest clear, privacy copy, tap targets, medication pathname mock; threads dispositioned; tip follows | merge-tree clean; vitest answer-thread/privacy/nav/medication/in-page-nav pass; CI pending after push | +| 2026-08-10 | PR #1788 | aaeb54630fde2c05efe9a90eda13fbb92cc933c0 | Run PR sweep | Static PR maintainability: extracted useAnswerThreadBootstrap (ClinicalDashboard 4144→4106); merged origin/main (#1794/#1795) | check:maintainability-budgets pass; vitest bootstrap+storage 17; tsc clean; CI pending after push | | 2026-08-10 | PR #1797 / claude/codex-m4a-retire-dead-type-8wq9ta | 6bf3c7b2a0600021290e165302fd07d721af6592 | retire the dead --text-2xl-compact type step (ledger #297): globals.css @theme, twMerge config, two test lists, the design-system-contract exemption, TOKENS.md/GATES.md | Executed the recorded next action on outstanding-issues #297. The step had zero class-utility and zero var(--text-*) consumers, so the deletion renders identically; UNUSED_TYPE_STEP_EXEMPTIONS is now empty and the declared-but-unconsumed gate holds the line with no carve-out. One test fixture using the token as a synthetic var() consumer was repointed at --text-2xl-minus. GATES.md corrected to eight non-standard steps; the 705-consumer total is unchanged because this step contributed 0. No clinical, RAG-ranking or operational risk paths touched (classifyPullRequestFiles: all false). | check:design-system-contract PASS (705 production files); check:type-scale --strict PASS; lint exit 0; typecheck exit 0; npm run build after rm -rf .next exit 0 (Compiled successfully in 63s); check:outstanding-issues PASS; verify:pr-local completed through typecheck then failed at test on a PRE-EXISTING root-permission failure in tests/pr-handoff-stop.test.ts that reproduces on clean d812c76 (5993 passed, 1 failed); build and check:rag:fixtures run/assessed separately. No UI gate: no rendered output can change. No provider-backed check run. | | 2026-08-10 | codex/visual-baseline-advisory-pr | 6bc57714c36bc6d027561bb8f5f8b00bb92524b2 | PR #1791 babysit unblock | fixed Production UI formulation Clear→Draft flake settle; classified visual drift vs non-drift failures | test:ci-workflows 263; classify-visual-baseline-outcome+ci-cache-safety 40 | -| 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 #1788 / codex/chat-contextual-back-answer-cache-05ea-1 | 98dd877ab4bd41e169310004c3b91aa4780d3772 | Run PR sweep | before: Production UI (2) failed on Breadcrumb/Medications selector; after: use Back to medications aria-label + contract guard; disposition Codex/Sentry/CodeRabbit threads; merged origin/main | vitest in-page-nav-contract+answer-thread-storage 19p; format; merge-tree clean | | 2026-08-09 | origin/pr/1686 | a5cce760d73bd174dba200b53852568dcdb9be0d | PR #1686 CI testing perfection and merged rollout reconciliation | Merged required CI was green, but hosted evidence confirmed P2 shard imbalance, duplicated critical coverage, net-negative 1.09 GB cache transport, inactive container revision enforcement, duplicated workflow/build/browser work, and missing local npm-ci selection. Fixed locally on current main; no P0/P1. | Hosted run 31285952061 inspected; focused Vitest 55 passed plus browser-preflight 12 passed; CI workflow suite 256 passed; typecheck passed; CI scope, verification plan, shard parity, gate manifest, action pins, npm-ci dry-run, docs and outstanding-issues guards passed; no Playwright/browser run or provider mutation. | | 2026-08-10 | codex/ci-perfected-rollout-20260809 (PR #1789) | accbc7c6324b839112ff8df8f9b66d3557f2b98e | PR babysit | unblocked; merged origin/main (false-DIRTY behind-but-clean); fixed Codex P2 ui_changed for Playwright runner helpers; thread replied+resolved | ci-change-scope --self-test pass; merge-tree clean; no provider gates | | 2026-08-10 | codex/ci-perfected-rollout-20260809 (PR #1789) | bf437370441c43a35ec63353642b0180ba5beba6 | PR babysit | late sync: merged origin/main (#1793/#1794); behind-but-clean; prior tip CI green; no code fixes | merge-tree clean; format clean; prior tip PR required pass; no provider gates | @@ -874,7 +871,12 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | a6a5e4cd59352244163a5d6d5439c2bc40a7ff95 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | f8d7ef3faa397de7c9aecc61a66ec5428a3eeed3 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | e6eea4d9b677de6919165753b9ceae059bac1190 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | +| 2026-08-09 | cursor/smarter-meds-search-9c1b | 74c3ea7706802925040b2c5603a7140a54dc9cd3 | medications-catalog-search typos brands | shipped catalog-local typo/brand search; no RAG | npm run test: 5899 passed | +| 2026-08-09 | cursor/smarter-meds-search-9c1b | aced65e055892b0e2927b3999f95c6435102f610 | medications-catalog-search typos brands | main sync; catalog-local typo/brand search complete; no RAG | medications+route tests 49 passed; merge-tree clean | +| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | 94dd432c0f64fd0593ea68d15aaa612240e7cd1d | PR #1785 unblock/fix | synced origin/main (behind-but-clean); merge-tree clean; prior tip CI green except PR mergeability DIRTY; review threads already cleared | merge-tree clean; behind 0; prior abb827b1 PR required+Production UI green; focused meds tests to re-run after sync | +| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | b722c628ca05eb32190ac6355e8ee0537817621c | PR #1785 unblock/fix | synced origin/main (#1782); behind-but-clean DIRTY cleared; merge-tree clean; review threads clear; prior tip product CI green | merge-tree clean; behind 0; test:focused meds after sync | +| 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 | | 2026-08-10 | PR #1803 / claude/codex-m4b-shadow-tight-migration-53a8kn | b778a56e9c3fa7642a783dde85e1130559d71e24 | shadow-tight token migration onto the e1 elevation tier and alias retirement (#262 part 1) | Migrated all 150 var(--shadow-tight) occurrences across 71 files to var(--e1) (90 gated production sites across 48 files, 60 mockup); deleted all three alias declarations (:root, .dark, forced-colors); pinned legacyShadowAliases 220 to 127 with exact per-path counts, closing 3 aliases of re-accumulated slack; added a whole-stylesheet absence assertion (mutation-verified); updated GATES.md section 3 plus a new section 6, TOKENS.md section 6, design-system.md, both redesign direction docs, .design-sync/conventions.md and ledger #262. Verified in Chromium that the ckb-v2 tier override is picked up by the alias substitution, so the change is value-preserving; that check is recorded as a prerequisite for the remaining six aliases. | npm run verify:cheap (30 static gates plus lint plus typecheck green; design-system contract passed, legacy shadow aliases 127; unit suite 553/554 files, 6024 tests passed, 1 pre-existing root-permission failure in tests/pr-handoff-stop.test.ts reproduced on untouched base a16dd26); npm run format:check whole tree; targeted Chromium computed-style measurement. verify:ui not run, Playwright browser revision drift #255, delegated to CI Production UI. No provider-backed gates. | -| 2026-08-10 | PR #1788 | cc64d8356ca964ef2e550f4493f17778965ada79 | Run PR sweep | merged origin/main (resolved in-page-nav ContextualBackLink+rail); fixed restore metadata/signature/guest clear, privacy copy, tap targets, medication pathname mock; threads dispositioned; tip follows | merge-tree clean; vitest answer-thread/privacy/nav/medication/in-page-nav pass; CI pending after push | -| 2026-08-10 | PR #1788 | aaeb54630fde2c05efe9a90eda13fbb92cc933c0 | Run PR sweep | Static PR maintainability: extracted useAnswerThreadBootstrap (ClinicalDashboard 4144→4106); merged origin/main (#1794/#1795) | check:maintainability-budgets pass; vitest bootstrap+storage 17; tsc clean; CI pending after push | -| 2026-08-10 | PR #1788 / codex/chat-contextual-back-answer-cache-05ea-1 | 98dd877ab4bd41e169310004c3b91aa4780d3772 | Run PR sweep | before: Production UI (2) failed on Breadcrumb/Medications selector; after: use Back to medications aria-label + contract guard; disposition Codex/Sentry/CodeRabbit threads; merged origin/main | vitest in-page-nav-contract+answer-thread-storage 19p; format; merge-tree clean | 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..30d18627f0 100644 --- a/src/lib/catalog-search.ts +++ b/src/lib/catalog-search.ts @@ -24,6 +24,66 @@ export function compactSearchText(value: string) { return value.replace(/\s+/g, ""); } +function typoDistanceLimit(term: string) { + if (term.length >= 8) return 2; + // 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 + * 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; + 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 +99,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 +191,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 +223,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..d67f4e873d 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,20 @@ 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); + // 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); + }); + 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", () => {