diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b0b050ff5..745789af0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -627,3 +627,10 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-19 | cursor/pr-policy-body-cleanup-f46b (PR #942) + PR #933 closeout | 7c8e6aadf0890b143372fb96f13d9de47a416db9 | post-merge CI triage for #933 PR-policy red check | PR #933 product merge (`bd864de0`) already on main with green post-merge main CI (Static/Unit/Build/Production UI/SAST/Docker). Sole remaining red check on #933 was post-ready PR policy against a stale synced body with unchecked governance boxes (from leftover `PR_POLICY_BODY.md` introduced by #932). Token cannot edit merged PR bodies (403). Removed the stale template via #942 so Sync PR policy body no longer reapplies unchecked governance. Local composer regression 6/6 on main; reserve unit 11/11. No product regression. | Hosted #933 pre-merge + main push green; #942 required checks green then squash-merged; focused Chromium composer 6/6; reserve Vitest 11/11. No OpenAI/Supabase provider calls. | | 2026-07-19 | cursor/documents-search-header-3eab / PR #936 | a7feaa3033180b672cfafaaaf75dc75088ebf052 | documents search header redesign final review + merge readiness | No remaining high-confidence P0-P1. Implemented identity-first results chrome, unified Sort/type-filter/Library toolbar, removed documents Also-in-library strip, relocated ScopeAndGovernanceNotice under controls, fixed Prettier CI failure and memo-busting empty warnings default, synced accurate PR policy body then removed the stale leftover, and repeatedly merged origin/main so squash auto-merge is not blocked behind/dirty. Hosted required checks including Production UI passed on the integrated head. | Local: typecheck/lint/format; focused Playwright documents `@critical` + deferred source/admin + forms sort persistence; design-system/icon-scale/maintainability; build + RAG fixtures; verify:pr-local units with known pdf-extraction-budget env artifact also on clean main. Hosted: PR policy, Static, Unit, Build, Production UI, Advisory UI, PR required green. No OpenAI/live Supabase writes. | | 2026-07-19 | cursor/specifiers-results-ui-7850 (PR #941) | ef9bcf23bb93cbaddddeda3aedf08e6d037e2f18 | Specifiers results UI elevation final review + merge readiness | No high-confidence P0-P1. Ranking blurb removed; solid Aegean subnav kept vs soft family chips; diagnosis control content-sized with min-h-tap; match cards single Open hit target with motion-reduce and guarded typical language. Re-synced origin/main after #936 ledger append conflict. Residual risk: hosted Production UI must finish green on exact head; full verify:cheap still hits known container-only pdf-extraction-budget failures also seen on main. | Local: format:changed; lint/typecheck on touched files; design-system + icon-scale; vitest specifiers+reserve 10/10; Playwright ui-specifiers 5/5 including filter interactions; production build + client-bundle secret scan; offline RAG fixtures 36/21. PR marked ready for review. No OpenAI/live Supabase provider calls. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | 2df3181859345339d777ac814908b3095cea36fd | ModeHomeMain contentAlign fix for differentials mobile clipping | No high-confidence P0–P2. Fix correctly introduces exclusive `contentAlign` (`center` \| `start` \| `startOnPhone`), migrates therapy/formulation/specifiers off broken className justify overrides, top-aligns differentials results via `autoRunSearch`, and guards with static + Playwright fold assertions. Residual: DSM/forms/services still default-center; justify-strip leaves variant prefixes; Playwright `0.55*vh` upper bound may flake if header band grows; Chip compact overrides still dual-utility via `cn()`. | Static diff vs `origin/main`; inventory of all `ModeHomeMain` call sites; justify-strip regex probe; `npx vitest run tests/mode-home-main-align.test.ts` 4/4. Not run: Playwright/`verify:ui` (browser), `verify:cheap`/`verify:pr-local`, provider-backed checks. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | 74c370d81342dd729398dc2b40ba3158ea30f1db | follow-up review + residual polish (policy body, align API, UI flake) | Prior residuals closed: `PR_POLICY_BODY.md` rewritten for #938 (Sync PR policy body was overwriting with stale #932 text); `withoutJustifyUtilities` strips prefixed utilities; Chip uses exclusive `density` type scale; DSM/forms/services use `startOnPhone`; Best Answer fold bound uses header+240px; `ui-overlap` waits for a single `header#search`. No remaining high-confidence P0–P2 in the ModeHomeMain/differentials mobile scope. | `npx vitest run tests/mode-home-main-align.test.ts` 5/5; Prettier on touched files. Playwright focused rerun and hosted Production UI expected after push. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | b62d414ca9001fbc1ac0d50b315450e107751d67 | merge-readiness after policy + hosted UI | No remaining high-confidence P0–P2. PR description sync + `verify:ui` evidence keep PR policy green; hosted Production UI / PR required green on exact head. | Local: align Vitest 5/5; ui-overlap 12/12; differentials fold Playwright 1/1. Hosted: Production UI, Advisory UI, Static, Unit, Build, PR policy, PR required, Sync PR policy body all pass. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | f6682cc08eb0fcc0b8dd170d5c9358b3fedc0c0d | final meticulous review + safe-merge handoff | No high-confidence P0–P2. Forms/services now centre when unseeded/loading and use `startOnPhone` only when seeded (avoids sparse empty homes). CodeRabbit risk-line comment dispositioned as stale #932 body confusion. Residual: branch protection still requires a human approving review before squash auto-merge can land. | Local: align Vitest 5/5; focused Chromium overlap+fold 13/13; verify:cheap/pr-local unit stages 2953/2955 with only known container `pdf-extraction-budget` Python ENOENT failures; format/lint/typecheck/RAG fixtures pass; local `next build` blocked by running ensure server (hosted Build green). Hosted exact-head: Production UI, PR required, Unit, Static, Build, PR policy all SUCCESS. Enabled squash `--auto` merge. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | e177b5e877aa2f834b3a601fa37679fa3e77b0e3 | merge main (#933) + post-merge revalidation | Merged `origin/main` (Safari edge-to-edge #933) with ledger conflict resolved by keeping both review rows. No product conflicts; `contentAlign`, Chip density, and Best Answer fold asserts intact. No new P0–P2. Still blocked only by required human approving review; squash auto-merge remains enabled. | Local after merge: align+composer-reserve Vitest 9/9; focused Chromium dock-hide + fold + compare-dock 3/3. Hosted on merge head: Production UI, PR required, Unit, Static, Build, PR policy all SUCCESS. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | 3a775d4fd9a44e97388c0041cb421f06265a7721 | fresh final review (user-requested) + main sync (#937) | No high-confidence P0–P2. Product delta unchanged vs prior merge-ready head; #933 reserve + #938 contentAlign remain complementary. Merged `origin/main` (#937) cleanly. Follow-up: dropped `PR_POLICY_BODY.md` when syncing #942 so this PR does not reintroduce the stale template; live PR description already correct. Residual: required human approving review. | Local: align+composer-reserve Vitest 9/9; focused Chromium overlap+fold+compare 14/14. Hosted CI green on prior tip. | +| 2026-07-19 | PR #938 / `cursor/fix-differentials-results-top-d760` | efe472fac3489473e2465da2d92980508c20fd81 | merge main (#934 favourites) + #941 sync | Merged favourites guest-gate (#934) then Specifiers results UI (#941). Product conflicts none: kept `contentAlign="startOnPhone"` on Specifiers home while taking #941 results chrome. `ui-overlap` gotoHome wait conflict resolved with settled single-header short-circuit. No new P0–P2. Residual: human approving review required for squash auto-merge. | Local: align Vitest; favourites+app-modes+tools 42 tests; Chromium ui-overlap+ui-tools 95/95; verify:cheap unit stage green. Hosted CI green on pre-#941 tip; re-running after this merge. | diff --git a/docs/process-hardening.md b/docs/process-hardening.md index bbfbd24dc..5a1d0ad89 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -4,6 +4,7 @@ This document turns the current process review into phased, durable repo practic ## Repository cleanup follow-ups (2026-07-19) +- **ModeHomeMain vertical alignment (fixed in #938):** commit `39d14a51` made standalone mode homes a `flex-1 justify-center` shell. That centres short empty homes correctly, but when differentials (and any tall results view) reused the same wrapper, the top of the content was clipped above the phone scrollport — white gap under the header, Best Answer unreachable, first visible list card looking like rank “2”. Prefer `ModeHomeMain`’s `contentAlign` prop (`center` | `start` | `startOnPhone`); never pass `justify-*` via `className` (`cn()` does not merge Tailwind). Guards: `tests/mode-home-main-align.test.ts` plus the narrow-viewport Best Answer fold assertion in `tests/ui-tools.spec.ts`. - **High-priority local process ownership:** `scripts/run-eval-safe.mjs` still scans for and terminates residual repository processes through `cleanupResidualEvaluationProcesses()`. A superseded RAG safety worktree contained a narrower child-owned `terminateOwnedProcessTree(child.pid)` approach plus a regression test proving unrelated Vitest, Playwright, and Next processes remain untouched. Do not cherry-pick that stale worktree wholesale; isolate this process-ownership fix on current `main`, then verify it statically without starting a provider-backed evaluation. Modifying or exercising the eval workflow remains approval-gated. - **Provider-gated RAG safety ideas:** the same stale worktree contained conservative answer-quality thresholds, an evaluation cost-cap preflight, production-safety validation, deep-health assessment, and citation/vector proof tests. Its 754-line retrieval migration and route changes conflict with the later public-title privacy and migration chain and must not be replayed. If explicitly approved, rescope only the still-relevant preflight utilities and tests against current `main`; keep live OpenAI/Supabase validation separate. - **Semantic reranking rollout debt:** PR #901 keeps `RAG_SEMANTIC_RERANK_ENABLED=false`. Do not enable it until the provider-backed 36/36 retrieval-quality gate and an ambiguity-focused canary are explicitly approved and recorded. diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index b1ff08ed3..9c4bb75c4 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -322,11 +322,23 @@ function MatchBadge({ label }: { label: string }) { ); } -function Chip({ children, className }: { children: string; className?: string }) { +function Chip({ + children, + className, + density = "default", +}: { + children: string; + className?: string; + /** Comfortable chips use a single exclusive type scale — never dual text-* via cn(). */ + density?: "default" | "comfortable"; +}) { return ( @@ -489,12 +501,12 @@ function MobileResultCard({
{result.tags.slice(0, 2).map((tag) => ( - + {tag} ))} {result.tags.length > 2 ? ( - {`+${result.tags.length - 2}`} + {`+${result.tags.length - 2}`} ) : null}
@@ -526,6 +538,7 @@ function BestAnswerCard({ return (
{onToggle ? : null} -

+

{best.subtitle}

-
+
{visibleTags.map((tag) => ( - {tag} + + {tag} + ))} - {hiddenTagCount > 0 ? {`+${hiddenTagCount}`} : null} + {hiddenTagCount > 0 ? ( + + {`+${hiddenTagCount}`} + + ) : null}
); diff --git a/src/components/differentials/differentials-home-page.tsx b/src/components/differentials/differentials-home-page.tsx index 1ec6d6234..ca3847153 100644 --- a/src/components/differentials/differentials-home-page.tsx +++ b/src/components/differentials/differentials-home-page.tsx @@ -87,8 +87,13 @@ export function DifferentialsHomePage({ query = "", autoRunSearch = false }: Dif [router, routedSearchContext.queryMode, routedSearchContext.scopeFilters], ); + // `autoRunSearch` is true on /differentials?q=…&run=1 — that mounts the tall + // SearchResultsView. Empty homes stay centred; results must top-align or the + // Best Answer / query band are clipped above the phone scrollport. + const showingResults = autoRunSearch; + return ( - + + + + = { + // Short empty homes — centre in the visible canvas. + center: "justify-center pt-[clamp(1.25rem,4vh,2.25rem)] sm:pt-[clamp(1.75rem,5vh,3.25rem)]", + // Tall results / content — keep the top reachable on every breakpoint. + start: "justify-start pt-3 sm:pt-4", + // Content-rich homes that still fit after sm — top-align on phone only. + startOnPhone: "justify-start pt-3 sm:justify-center sm:pt-[clamp(1.75rem,5vh,3.25rem)]", +}; + +/** Strip bare and prefixed justify utilities (`sm:justify-center`, `max-sm:justify-start`, …). */ +function withoutJustifyUtilities(className?: string) { + if (!className) return undefined; + const cleaned = className + .replace(/(?:^|\s)(?:[\w-]+:)*justify-(?:normal|start|end|center|between|around|evenly|stretch)(?=\s|$)/g, " ") + .replace(/\s+/g, " ") + .trim(); + return cleaned || undefined; +} + +/** + * Standalone-route wrapper that mirrors the dashboard mode homes. The shell + * reserves composer clearance via --mobile-composer-reserve on #main-content. */ export function ModeHomeMain({ testId, children, className, + contentAlign = "center", }: { testId?: string; children: ReactNode; className?: string; + contentAlign?: ModeHomeMainAlign; }) { return (
{children} diff --git a/src/components/services/services-home-page.tsx b/src/components/services/services-home-page.tsx index 9c4bb24ce..62218cd1a 100644 --- a/src/components/services/services-home-page.tsx +++ b/src/components/services/services-home-page.tsx @@ -118,7 +118,12 @@ export function ServicesHomePage({ defaultServiceSlug = null }: { defaultService ) : null; return ( - + + + { + const modeHomeSource = readFileSync(resolve(SRC_ROOT, "components/mode-home-template.tsx"), "utf8"); + const differentialsPageSource = readFileSync( + resolve(SRC_ROOT, "components/differentials/differentials-home-page.tsx"), + "utf8", + ); + + it("owns exclusive justify alignment via contentAlign (cn cannot merge Tailwind)", () => { + expect(modeHomeSource).toMatch(/export type ModeHomeMainAlign/); + expect(modeHomeSource).toMatch(/MODE_HOME_MAIN_ALIGN_CLASS/); + expect(modeHomeSource).toMatch(/withoutJustifyUtilities/); + expect(modeHomeSource).toMatch(/center: "justify-center/); + expect(modeHomeSource).toMatch(/start: "justify-start/); + expect(modeHomeSource).toMatch(/startOnPhone: "justify-start/); + // Must strip responsive/prefixed justify utilities, not only bare ones. + expect(modeHomeSource).toContain("(?:[\\w-]+:)*justify-"); + // Alignment must win over consumer className — apply align map last. + expect(modeHomeSource).toMatch( + /withoutJustifyUtilities\(className\),\s*MODE_HOME_MAIN_ALIGN_CLASS\[contentAlign\]/, + ); + }); + + it("strips bare and prefixed justify utilities from className", () => { + // Mirror withoutJustifyUtilities — keep in sync with mode-home-template.tsx. + const strip = (className: string) => + className + .replace(/(?:^|\s)(?:[\w-]+:)*justify-(?:normal|start|end|center|between|around|evenly|stretch)(?=\s|$)/g, " ") + .replace(/\s+/g, " ") + .trim(); + + expect(strip("px-4 justify-center sm:px-6")).toBe("px-4 sm:px-6"); + expect(strip("sm:justify-center max-sm:justify-start gap-2")).toBe("gap-2"); + expect(strip("lg:justify-between justify-end")).toBe(""); + }); + + it("top-aligns differentials search results and keeps the empty home centred", () => { + expect(differentialsPageSource).toMatch(/const showingResults = autoRunSearch/); + expect(differentialsPageSource).toMatch(/contentAlign=\{showingResults \? "start" : "center"\}/); + }); + + it("migrates content-rich homes off fragile className justify overrides", () => { + const alwaysStartOnPhone = [ + resolve(SRC_ROOT, "components/therapy-compass/screens/home-screen.tsx"), + resolve(SRC_ROOT, "components/formulation/formulation-home-page.tsx"), + resolve(SRC_ROOT, "components/specifiers/specifiers-home-page.tsx"), + resolve(SRC_ROOT, "components/dsm/dsm-home-page.tsx"), + ].map((path) => readFileSync(path, "utf8")); + + for (const source of alwaysStartOnPhone) { + expect(source).toMatch(/contentAlign="startOnPhone"/); + expect(source).not.toMatch(/ModeHomeMain[^>]*className="[^"]*justify-/); + } + + // Forms/services only top-align when the registry is seeded; short empty / + // loading notices stay centred so the phone canvas does not look sparse. + for (const path of [ + resolve(SRC_ROOT, "components/forms/forms-home-page.tsx"), + resolve(SRC_ROOT, "components/services/services-home-page.tsx"), + ]) { + const source = readFileSync(path, "utf8"); + expect(source).toMatch(/contentAlign=\{hasRegistryRecords \? "startOnPhone" : "center"\}/); + expect(source).not.toMatch(/ModeHomeMain[^>]*className="[^"]*justify-/); + } + }); + + it("forbids ModeHomeMain className justify-* overrides across src/", () => { + // Regression: 39d14a51 made ModeHomeMain a flex-1 justify-center shell. + // Call-site className="justify-start …" looked like a fix but cn() leaves + // both utilities in the class string, so CSS source order decides — flaky. + const offenders: string[] = []; + for (const filePath of walkTsxFiles(SRC_ROOT)) { + const source = readFileSync(filePath, "utf8"); + if (!source.includes("ModeHomeMain")) continue; + const matches = source.matchAll(//g); + for (const match of matches) { + const attrs = match[1] ?? ""; + if (/className=\{?["'`][^"'`]*justify-/.test(attrs) || /className=\{[^}]*justify-/.test(attrs)) { + offenders.push(`${filePath.replace(`${process.cwd()}/`, "")}: ModeHomeMain className contains justify-*`); + } + } + } + expect(offenders).toEqual([]); + }); +}); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index efad0b850..5676377e5 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1474,6 +1474,34 @@ test.describe("Clinical KB tools launcher", () => { expect(badgeMetrics.height).toBeGreaterThanOrEqual(22); expect(badgeMetrics.scrollHeight).toBeLessThanOrEqual(badgeMetrics.height + 1); + // Tall results must be top-aligned: Best Answer stays reachable at scrollTop 0. + const mainContent = page.locator("#main-content"); + await expect.poll(() => mainContent.evaluate((element) => element.scrollTop)).toBe(0); + const bestAnswer = page.getByTestId("differential-best-answer"); + await expect(bestAnswer).toBeVisible(); + const foldLayout = await bestAnswer.evaluate((best) => { + const main = document.querySelector("#main-content"); + const header = document.querySelector("header.universal-header"); + if (!main) return null; + const bestRect = best.getBoundingClientRect(); + const headerBottom = header?.getBoundingClientRect().bottom ?? main.getBoundingClientRect().top; + return { + scrollTop: main.scrollTop, + bestTop: bestRect.top, + bestBottom: bestRect.bottom, + headerBottom, + viewportHeight: window.innerHeight, + }; + }); + expect(foldLayout).not.toBeNull(); + expect(foldLayout!.scrollTop).toBe(0); + // Best Answer must start in the visible fold under the chrome — not clipped + // above the scrollport (the ModeHomeMain justify-center regression). Bound + // to the header band rather than a tight viewport fraction so tall chrome / + // safe-area insets do not flake the upper-half check. + expect(foldLayout!.bestTop).toBeGreaterThanOrEqual(foldLayout!.headerBottom - 2); + expect(foldLayout!.bestTop).toBeLessThan(foldLayout!.headerBottom + 240); + // Phone list hides the featured best answer, so ranks must start at 1. const mobileCards = page.getByTestId("differential-mobile-result-card"); await expect(mobileCards.first()).toBeVisible();