PR: Production readiness improvements and E2E fixes - #105
Conversation
…g issues for production readiness
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 227dd0ad0a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e cohorts, and clinical split
… monitoring ranges, and extractive quality filtering
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a3c82c4100
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Tier 1 (reliability): - logger.ts: redacting structured logger (secrets/PII) wired into http.ts, the answer-stream error sink, and upload cleanup - /api/health readiness probe (config presence + optional deep Supabase check) - upload magic-number content verification + orphaned-cleanup logging - audit_logs writes on document upload/rename/delete (src/lib/audit.ts) - supabase migrations: rag_queries retention purge via pg_cron + audit_logs table Tier 2/3: - CI runs test:coverage; vitest coverage thresholds as a regression floor - production-readiness preflight fails on demo mode in a prod-like env - tsconfig target ES2017 -> ES2022 Tests: logger, file-signature, health-route, audit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The verify job was failing because the settings dialog (mobilePlacement="top") applied animate-sheet-up on mobile, starting the panel at translateY(100%) off-screen. Playwright captured the bounding box mid-animation at y≈118, failing the assertion y<=24. Fix: for defaultSheetIsTopAligned, use animate-pop-in on mobile instead. The pop-in animation scales from 0.97 at centre, keeping the top edge at y≈12+height*0.015 (≤23.94 for max-h 796px), which satisfies y<=24 at every frame.
#1376) * perf: cut fixed per-request round trips and add preamble Server-Timing Five latency fixes from the 2026-07-28 audit (docs/audit/latency-audit-2026-07-28.md). Rebased fresh on main; the index migration that sank PR #1312 is split out. RAG impact: no retrieval behaviour change — the only src/lib/rag/** edit defers a process-local cache write off the response path. No scoring, ordering, selection, alias, or citation logic is touched, and the mid-request staleness guard that discards the write when the corpus moves is preserved. - Server-Timing: /api/answer/stream emitted none at all, and stream/route.ts:262 confirms it is the route the UI actually uses. Adds auth/ratelimit there and to /api/search, plus scope on /api/answer. On a streaming route only pre-flush stages can reach a header; routing in-stream stages through the SSE contract would put instrumentation inside a governed clinical payload. - Shared-cache hits no longer await the cache write. setCachedAnswer forces an uncached documents read, so the fastest path in the system paid a round trip before responding. Deferred, not dropped. - /api/answer overlaps scope resolution with the rate-limit RPC and aborts it on deny, so a throttled caller still costs nothing. Threading the signal also fixes scope queries never receiving .abortSignal(). - document_table_facts: three select("*") narrowed to explicit projections, keeping the generated search_tsv and owner_id off the wire. The PATCH response is unchanged — the projection matches the TableFactRow DTO field for field. - 10 of 11 ssr:false dashboard surfaces had no loading fallback and rendered nothing between HTML arrival and chunk execution; all now use the shared LoadingPanel. Adds preconnect/dns-prefetch for the Supabase origin, which AuthProvider contacts on mount with no connection warm-up. Ledger #98-#105 record the 19 findings not addressed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: address Codex P1s and restore docs link CI Keep resolveSearchScope behind the answer rate limiter so denied callers cannot enumerate documents/labels, and discard deferred shared-cache promotions that resume after invalidateRagCachesForOwner via an invalidation epoch. Drop the outstanding-issues reference to the unauthored L2-3 migration path that broke docs:check-links. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): record pr-1376 CI/bugbot repair review Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): supersede pr-1376 repair record at tip HEAD Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * style: prettier-format files that failed format:check Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * fix: harden cache invalidation epochs and close review follow-ups Scope invalidation epochs per owner, await shared-cache promotion with post-write cleanup on raced invalidation, thread the stream abort signal into resolveSearchScope, emit Server-Timing on empty-scope answers, and tighten the rate-limit gating test synchronization. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs(ledger): record pr-1376 follow-up repair at tip Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> * docs: correct three audit claims and record the token-streaming refutation Corrections found while planning the remaining work — each was verified at the cited line, and each REDUCES the work the audit implied: - L3-3 was wrong twice: the Therapy Compass index and full payloads are mutually exclusive (mode-home first paint is 707 KB, not 3.16 MB), and the index split the finding implied was missing already ships via build-therapies-index.mjs with its own gate. The real residual is only the missing /public Cache-Control and the proxy matcher not excluding .json. - L3-6(b) overstated a 3-hop first-paint waterfall. The 4-way fan-out is gated on drawer visibility, and readLocalProjectIdentity is a guaranteed no-op off localhost. Real production win is ~1 hop. - L3-6(c) proposed batching as new work, but /api/images/signed-urls already exists with zero call sites. Records the hard constraint that signed URLs must never reach SearchResult.images[].signed_url, because rag-cache persists results into a shared cross-owner cache table. - The measurement plan named test-cache-path.mjs as an offline fixture; it is the vitest/tsc cache-directory helper. Names the real offline surface and the vi.doMock seam instead. Also adds Refutation 6 to docs/rag-behaviour/refuted-approaches.md: token streaming as a latency fix. Unlike Refutations 1-5 this was not re-attempted — it is recorded because the defect it would "fix" is real and visible, so the wrong fix is the one a future task reaches for first. answer-stream-contract.ts removed token/revising as a clinical-safety control; raw tokens bypass the numeric-faithfulness gate. Only admissible shape is progressive disclosure of already-verified sections over the existing progress event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep shared-cache promote off the cold-path response Awaiting replaceSharedCacheRow inside setCachedAnswer blocked every generation path on a Sydney DB round trip. Restore fire-and-forget shared promote while keeping owner-scoped epoch cleanup after commit. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Confirms the squash merge landed by content: the diff between squash commit f4a3f50 and branch tip bebc6c0 is empty, so nothing was orphaned by the auto-merge race despite four concurrent pushes during the merge window. Also records that the coverage follow-ups were renumbered #98/#99 -> #106/#107 (main claimed #98-#105 concurrently) with both rows verified intact, and that the three red `PR required` results were concurrency cancellations rather than defects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b
…iew batch Eight review findings (1 Codex P2, 7 CodeRabbit), each verified against the code before acting. MOST CONSEQUENTIAL — the L2-3 "recall is byte-identical" claim was wrong, and it was load-bearing. fetchDocumentTitleAliasRows (rag-candidate-sources.ts:482) applies .limit(12) with no ORDER BY, so which twelve documents return is plan-dependent; a new index can change the title-alias set feeding candidate assembly. "No query text changes" is true, but recall does not follow from it. That claim was the argument for keeping L2-3 out of canary territory, so the gating is revised: the documents-list and (status,id) uses stay ordering-safe, the RAG-path index is canary-gated unless the unordered .limit(12) is made deterministic first — the cheaper fix, since an unordered LIMIT is latent nondeterminism regardless of this work. Operator SQL alone never reaches staging, DR, or local replay: migrations/ is the source of truth and schema.sql only a mirror, so hand-run statements hit the live database and nothing else, and a required_indexes registration would fail on every replayed environment. Authoring the migration is now a required part of #102, following the 20260717170000 idempotent pattern. This PR still ships no migration (the #1312 objection), but the runbook no longer implies the operator sequence is sufficient. Test guard hardened: the ordering case anchored on a fixed 5 ms sleep, which can expire before the handler reaches the limiter. It now waits for consumeSubjectApiRateLimit to be entered, then asserts scope is untouched — the same guarantee without the timing fragility. Ledger: the 78e2beb record still described the reverted scope-vs-ratelimit overlap. The ledger is append-only, so this appends a superseding record via ledger:append --supersede rather than editing the row, per the contract. Status wording: #102 is "runbook prepared", not done, while the operator steps are pending; #105 separates shipped implementation from pending browser verification; #98/#99/#102/#103/#105 restored to the execution queue with their remaining actions. Fixed the MD038 malformed RAG-impact code span. Verification: verify:cheap exit 0 (427 files, 4386 passed / 4 skipped); check:branch-review-ledger pass (1232 records, no duplicates). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
Resolved docs/outstanding-issues.md by keeping this branch's updated #98, #102, #104 and #105 entries (the ones this PR rewrites) and adding main's new #108 and #109 rows; the rest of the conflict was prettier re-padding column widths after a summary-cell length change. next-id stays at 110. Removed four exact-duplicate branch-review-ledger rows produced by the union merge driver keeping both copies of rows present on this branch and on main - the duplication risk tracked as #88.
Resolves the `docs/outstanding-issues.md` conflict. Two independent causes: 1. Rows #98–#105 diverged. `main` carries the newer text (PR #1377 landed "Done 2026-07-29" outcomes and rewrote several summaries); this branch carried the pre-#1377 wording. Took `main`'s rows wholesale — the branch made no edits to that range, it simply forked before them. 2. `#108`/`#109` were allocated twice. `main` used them for the verified-landed branch deletions and the shallow-clone finding; this branch had used the same two ids for the design-system token manifest and the `ui-overlap` flake. The `issues:next-id` marker has no concurrency protection, which is exactly the failure the branch's own PR notes predicted. Renumbered this branch's two rows to #110/#111 and bumped the marker to 112. No content was dropped from either side; all 111 rows are unique. Also records #111 as done, since this branch is what fixes it: the ui-overlap inset measurement now retries inside `toPass` with the 2px symmetry tolerance and the assertions unchanged. Leaving it open with a "Next: apply the retry shape" action would have re-queued work this PR already did.
Main's #1461 reflowed the open-items table while this branch appended #17's live Web-Vitals verdict, so the two sides conflicted for real (`git merge-tree` dirty, not staleness). Resolved row by row rather than by taking either side: kept this branch's #17 BREACH verdict and #105 preconnect evidence, took main's #86 X3 progress and its new #145 row. #105 closes. Its remaining half — the `LoadingPanel` fallbacks — is now verified, and by a different method than the row prescribed. It claimed the fallback renders solely while the client chunk is in flight, so only a throttled-network run could observe it. That is wrong: the installed Next 16 loader wraps an `ssr:false` import in Suspense whenever a `loading` element is supplied, and `BailoutToCSR` throws on the server, so the fallback is emitted in the server response HTML. Confirmed against the running dev server — `role="status" aria-label="Loading"` appears 1x on `/`, 2x on `/dsm`, 2x on `/forms`. No throttled run and no `verify:ui` were needed. Finding raised by Codex on PR #1459. The two recommended-queue entries now match their detail rows: #105 is dropped from the queue, and #17 no longer directs a reader to capture evidence that has already been captured and graded — its next action is ranking the mobile findings by measured contribution and cross-checking the raw Lighthouse JSON artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
Second real conflict on docs/outstanding-issues.md, this time in the archive table: main's #1455 archived #122 while this branch archived #105. Kept both rows and took main's separator width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
…d the wrong component The claim that `LoadingPanel` was verified present in the SSR response HTML does not hold, so #105 is reopened rather than archived. The evidence was `role="status" aria-label="Loading"` appearing in the initial HTML of `/`, `/dsm` and `/forms`. Those hits are real, but that exact string has exactly one renderer in `src/` — `ModeHomePageSkeleton` (`src/components/mode-home-page-skeleton.tsx:8`). `LoadingPanel` never emits a bare "Loading" label; it renders `aria-label={label}`, always a specific string such as "Loading differentials". Re-checked per route against a running server, `/`, `/dsm`, `/forms`, `/differentials` and `/favourites` return zero `aria-label="Loading <something>"` matches. Codex's mechanism finding on this PR stands and is not what failed: the installed Next 16 loader does set `hasSuspenseBoundary` when a `loading` element is supplied, and `BailoutToCSR` throws on the server, so a mounted `ssr:false` surface emits its fallback into the response HTML. What was missing is that this only fires for a surface mounted during the initial render, and none of the eleven in `clinical-dashboard-lazy.tsx` are — every one sits behind interaction state (`ClinicalDashboard.tsx:3666-4070`). So neither a cold-load grep nor the throttled check the row originally prescribed can observe them; that needs a driven browser session. The reopened row and its queue entry both record the withdrawn claim and carry an explicit stop rule against re-closing on a cold-load grep. The preconnect half remains verified and is marked so, to avoid redoing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
Fourth genuine conflict on docs/outstanding-issues.md, confirmed with git merge-tree before acting. Main's #1470 rewrote #13 as measurement-gated while this branch changed #17, #105 and added #147. Verified by hash that main touched neither #17 nor #105 — both are byte-identical at the merge base and at origin/main — so those rows take this branch's version and #13 takes main's, rather than either side winning wholesale. Also de-duplicated against main's new #118, which already owns adopting the Lighthouse baseline and flipping enforce. #147's gate paragraph now cross-references it instead of restating it, and contributes the constraint #118 was missing: pinning a baseline today would bake CLS 0.363 on /dsm in as the accepted value, so the CLS fixes must land first or the gate ratifies the breach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
Fifth consecutive genuine conflict on docs/outstanding-issues.md, confirmed with git merge-tree before acting. Main's d8b034c (PR #1441) deleted three rows from the Open items table and archived none of them: #85, which that PR genuinely resolved by adding check-upload-limit-parity.mjs; #119, a duplicate of #122; and #105, which is open and unresolved — its LoadingPanel half is still unverified. This resolution mirrors main on #85 and #119, which were dispositionable, and keeps #105, which was not. #105 survived only because this resolution was done row by row and then diffed with column padding normalised away. Taking either side wholesale would have lost it silently, which is what AGENTS.md forbids for this file and what appears to have happened upstream. Filed as #148: check:outstanding-issues validates duplicate ids, ids in both tables, stale markers and malformed rows, but never compares against the previous revision, so a row that simply disappears is invisible to it in verify:cheap and static-pr alike. The fix is a deletion check against the merge base, not a union merge driver — #133 removed that driver deliberately and reinstating it would bring back the duplicate-row damage it was removed to stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
…ation, restore #105 after an upstream deletion (#1459) * docs(issues): record #105's preconnect verification in a browser The preconnect half of #105 is verified; the LoadingPanel half is not, and the row now says which is which rather than carrying one PENDING for both. Worth recording how it was verified, because the obvious check silently proves nothing: locally NEXT_PUBLIC_SUPABASE_URL is unset, so supabaseOrigin() returns null and the layout renders no link at all. Loading the app and finding no preconnect would have looked like a failure and been an absent fixture. The check needs a placeholder origin (no real network call — the point is React's hoisting, not Supabase connectivity). With that set, both the SSR HTML and the live browser DOM show preconnect and dns-prefetch in <head> with zero stray preconnect in <body>, so React 19 does hoist them out of the body where they are authored. crossOrigin="anonymous" survives the hoist, which matters: without it supabase-js's CORS fetches open a second connection and the hint buys nothing. The remaining half needs a different technique. A LoadingPanel fallback only renders while a lazy chunk is in flight, so it needs a throttled-network browser check rather than verify:ui, which would race past it. Gates: outstanding-issues guard 142 rows / unique ids; prettier clean; the edit is a 1-line diff because it stays inside the Detail column's existing width (1133 chars against a 3268 budget) — see #133 for why that matters in this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF * docs(issues): record #17's live Web-Vitals verdict — mobile breaches Dispatched live-web-vitals.yml against psychiatry.tools (run 30548662649, 5 routes x 3 samples). #17 has been the gate on seven latency findings for weeks with no evidence behind it. It now has evidence, and it is a breach. Mobile medians against LCP<2500ms / CLS<0.1: root 4308ms 0.023 therapy-compass 4716ms 0.142 documents-search 3676ms 0.220 dsm 3639ms 0.363 forms 3786ms 0.212 Desktop passes everywhere (LCP 625-694ms, CLS 0.016-0.097), which is worth noting because desktop-only checking would have concluded the opposite. The verdict is gradeable rather than noisy: every LCP sample range sits wholly above 2500ms, so the straddle rule #114 added does not fire. That distinction is the whole point of #1407's sampling work — a single run per cell could not have told a real breach from variance. Applying the rule as written before the numbers were read: any breach makes only the breaching routes' findings actionable, and NOTHING becomes WONTFIX. That is the opposite of what a favourable sample would have produced, which is exactly why the rule was written first. CLS 0.363 on /dsm is the worst single number and is visible layout shift, not a byte-count problem — so it is not addressed by the payload findings #17 gates. Recorded honestly as one step short of complete: this reads the summariser's emitted table, not the raw Lighthouse JSON (artifact 8762211043, 30-day retention). The row itself asks for that cross-check before recording a verdict, most of all a PASS; this is a breach, so the risk of the table flattering the result is lower, but the check is still outstanding. INP is absent because Lighthouse cannot measure it in lab conditions. Diff stays 1 line: #17's Detail cell is 2184 chars against the column's 3268 budget, so nothing repads (see #133). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF * docs(issues): withdraw the #105 LoadingPanel verification — it matched the wrong component The claim that `LoadingPanel` was verified present in the SSR response HTML does not hold, so #105 is reopened rather than archived. The evidence was `role="status" aria-label="Loading"` appearing in the initial HTML of `/`, `/dsm` and `/forms`. Those hits are real, but that exact string has exactly one renderer in `src/` — `ModeHomePageSkeleton` (`src/components/mode-home-page-skeleton.tsx:8`). `LoadingPanel` never emits a bare "Loading" label; it renders `aria-label={label}`, always a specific string such as "Loading differentials". Re-checked per route against a running server, `/`, `/dsm`, `/forms`, `/differentials` and `/favourites` return zero `aria-label="Loading <something>"` matches. Codex's mechanism finding on this PR stands and is not what failed: the installed Next 16 loader does set `hasSuspenseBoundary` when a `loading` element is supplied, and `BailoutToCSR` throws on the server, so a mounted `ssr:false` surface emits its fallback into the response HTML. What was missing is that this only fires for a surface mounted during the initial render, and none of the eleven in `clinical-dashboard-lazy.tsx` are — every one sits behind interaction state (`ClinicalDashboard.tsx:3666-4070`). So neither a cold-load grep nor the throttled check the row originally prescribed can observe them; that needs a driven browser session. The reopened row and its queue entry both record the withdrawn claim and carry an explicit stop rule against re-closing on a cold-load grep. The preconnect half remains verified and is marked so, to avoid redoing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF * docs(issues): rank #17's mobile findings — CLS reproduces exactly offline #17 asked for the mobile breach to be ranked by measured contribution. Done, and the measurement turned up something that changes what the work costs. Ran the local offline production Lighthouse harness over the same five routes the live dispatch used. The mobile CLS numbers do not merely correlate with production — they match run 30548662649 to three decimals on all four locally measurable routes (0.023 / 0.142 / 0.220 / 0.363), and the desktop range matches at both endpoints (0.016-0.097). /forms did not measure locally (Lighthouse NO_NAVSTART, its own "run again" transient); live it was 0.212. So CLS on these routes is deterministic layout structure, not network or production data. The CLS half of #17 is debuggable and gate-able locally at zero provider cost, and #17's recorded premise that production vitals need Supabase secrets is stale — run-lighthouse-budget.mjs builds and serves an offline production app precisely to sidestep that. Its earlier "dev-mode CLS is excellent at 0.00-0.04" note was measuring dev, which does not reproduce production. LCP does NOT reproduce and must not be read from local runs: 2014-2051 ms local against 3639-4716 ms live, because the loopback server has no network latency. The LCP ranking does survive — /therapy-compass is the outlier both locally and live — which corroborates #117. Filed as #147 with the ranking (/dsm 0.363 worst, then /documents/search 0.220, /forms 0.212, /therapy-compass 0.142, / 0.023 passing) and the next step: Lighthouse's layout-shift-elements audit returned zero items on every route, so element attribution needs a PerformanceObserver on layout-shift against the same offline build. Two reproduction traps recorded with it: the harness reads CHROME_PATH but nothing in this container sets it, so chrome-launcher fails on every route; and a truncated `progress` package in the npx cache broke Lighthouse startup entirely. No budget baseline was committed — the grader correctly refused this run as incomplete evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF * docs(review): record PR 1459 final reconciliation --------- Co-authored-by: Claude <noreply@anthropic.com>
Resolves the fifth conflict on docs/outstanding-issues.md by taking main's side on both hunks, after verifying that was correct rather than convenient: main renumbered the queue to fix a duplicate rank 18 this branch introduced, and it archived #138 and #146 rather than dropping them (confirmed present in the archive table). The resulting id set is byte-identical to main's. #150 escalated. That row records CodeRabbit reviewing none of a full day's PRs and rests explicitly on the Codex connector being the surviving reviewer that "found three real defects that had survived local gates". On PR #1505 Codex posted its own usage-limit notice alongside CodeRabbit's spending-cap notice, so that PR received zero automated review and so will anything opened while both caps hold. This is the same issue with its fallback removed, not a new one — which matters because the single Codex finding on #1459 was correct and caught a verification that had matched the wrong component, closing #105 on bad evidence. Local gates did not catch it. The attribution harness lands as scripts/measure-cls-attribution.mjs. #147's next step needs a before/after CLS pair, and #118 will want element attribution again; without this, both mean re-deriving it. Build and serve mirror run-lighthouse-budget.mjs so the numbers sit beside the Lighthouse reports. Two traps that cost real time are encoded rather than left to be rediscovered: CHROME_PATH must be set where the browser is outside a standard location, and an init script attaching a MutationObserver to document.documentElement before <html> exists throws and silently takes the CLS observer with it — so the script now fails loudly on an all-zero result instead of reporting a false clean bill. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01361jh3eYVjJCzXWjAhdZiF
Resolves type/lint warnings, missing BadgeCluster/ClinicalBadge components in the medication workspace, and timing issues in E2E tests. Kept open for review.