diff --git a/.claude/skills/handoff/SKILL.md b/.claude/skills/handoff/SKILL.md
index f22a85d91d..38472dc230 100644
--- a/.claude/skills/handoff/SKILL.md
+++ b/.claude/skills/handoff/SKILL.md
@@ -28,6 +28,12 @@ force-push, or discard work.
- Default: `npm run verify:pr-local` (format + cheap gate, plus build/RAG when the
scope needs them).
- Touched UI/routing/styling: add `npm run verify:ui`.
+ - Touched `src/app/`, `src/components/`, or `tests/` (or design-system adoption
+ inputs): run `npm run design-system:adoption:update` and stage any regenerated
+ `docs/design-system/adoption-manifest.json` / marked COMPONENTS/ADOPTION sections
+ before push. Pre-commit syncs this when hooks are installed; Cloud agents that
+ bypass hooks still need the explicit update or static-pr + coverage fail together
+ (PR #1782).
- Touched Supabase env/config: `npm run check:supabase-project` (provider — confirm first).
Do not claim a gate passed unless it actually ran.
4. **Commit** with a clear message. End the message with:
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
index f92c9ab0df..2aed7f2ddc 100755
--- a/.githooks/pre-commit
+++ b/.githooks/pre-commit
@@ -23,6 +23,7 @@ matches_staged() {
sync_sitemap=0
sync_inventory=0
check_module_map=0
+sync_design_system_adoption=0
if matches_staged '^(src/app/|mockups/|src/lib/(app-modes|document-flow-routes|differentials|dsm|formulation|forms|services|specifiers|therapies)\.ts$|scripts/generate-site-map\.ts$|docs/site-map\.md$)'; then
sync_sitemap=1
@@ -33,7 +34,13 @@ fi
if matches_staged '^(src/app/|src/lib/|supabase/schema\.sql$|docs/codebase-index\.md$)'; then
check_module_map=1
fi
-if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ]; then
+# Adoption proof indexes every tests/ + src/components|/src/app import edge.
+# Stale docs/design-system/adoption-manifest.json fails both static-pr and unit
+# coverage (seen on PR #1782 after a DocumentViewer DOM-test change).
+if matches_staged '^(src/app/|src/components/|tests/|docs/design-system/(adoption-contract|adoption-manifest)\.json$|docs/design-system/(ADOPTION|COMPONENTS)\.md$|scripts/generate-design-system-adoption\.mjs$|\.design-sync/)'; then
+ sync_design_system_adoption=1
+fi
+if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then
exit 0
fi
@@ -51,6 +58,10 @@ if [ "$check_module_map" = "1" ]; then
module_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|src/lib/|supabase/schema\.sql$)' || true)
mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$module_inputs" | sed '/^$/d' | sort -u)
fi
+if [ "$sync_design_system_adoption" = "1" ]; then
+ adoption_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|src/components/|tests/|docs/design-system/(adoption-contract|adoption-manifest)\.json$|docs/design-system/(ADOPTION|COMPONENTS)\.md$|scripts/generate-design-system-adoption\.mjs$|\.design-sync/)' || true)
+ mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$adoption_inputs" | sed '/^$/d' | sort -u)
+fi
if [ -n "$mixed_inputs" ]; then
echo "[pre-commit] Documentation inputs have unstaged or untracked changes:" >&2
printf '%s\n' "$mixed_inputs" >&2
@@ -68,6 +79,9 @@ fi
if [ "$check_module_map" = "1" ]; then
docs_to_check="$docs_to_check docs/codebase-index.md"
fi
+if [ "$sync_design_system_adoption" = "1" ]; then
+ docs_to_check="$docs_to_check docs/design-system/adoption-manifest.json docs/design-system/COMPONENTS.md docs/design-system/ADOPTION.md"
+fi
# Generators rewrite whole files. Refuse before running them when a selected
# output has an unstaged edit, otherwise regeneration can erase local work and
@@ -108,7 +122,7 @@ fi
# Re-check after dropping a task: an empty docs_to_check would make the trailing
# unstaged-docs diff match every modified file in the tree.
-if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ]; then
+if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then
exit 0
fi
@@ -123,6 +137,22 @@ fi
if [ "$check_module_map" = "1" ]; then
npm run docs:check-index
fi
+if [ "$sync_design_system_adoption" = "1" ]; then
+ if [ ! -f scripts/generate-design-system-adoption.mjs ]; then
+ if git cat-file -e "HEAD:scripts/generate-design-system-adoption.mjs" 2>/dev/null; then
+ echo "[pre-commit] scripts/generate-design-system-adoption.mjs was removed or renamed by the pending commit; refusing to skip adoption synchronization." >&2
+ exit 1
+ fi
+ echo "[pre-commit] scripts/generate-design-system-adoption.mjs is absent from this worktree - skipping adoption sync." >&2
+ sync_design_system_adoption=0
+ docs_to_check=$(printf '%s\n' $docs_to_check | grep -Ev '^docs/design-system/(adoption-manifest\.json|COMPONENTS\.md|ADOPTION\.md)$' || true)
+ if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ] && [ "$sync_design_system_adoption" = "0" ]; then
+ exit 0
+ fi
+ else
+ npm run design-system:adoption:update
+ fi
+fi
unstaged_docs=$(git diff --name-only -- $docs_to_check)
if [ -n "$unstaged_docs" ]; then
diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 1d27ef0012..e261c05c7f 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -837,16 +837,19 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0e77e7bc0842bef4ffc045c6dbea4626152490d1 | differentials diagnosis term links | implemented exact+alias termLinks chips on diagnosis+presentation pages; vitest 58/58; verify:pr-local green | vitest differential-diagnosis-links+detail+section-nav+route; verify:pr-local; ensure spot-check |
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | 0daa9e2f9fc84e879fd661da94203568309234a6 | PR #1768 Autopilot+Bugbot review-and-fix | Merged origin/main (DIRTY was ledger+detail-page staleness; merge-tree clean). Fixed SEGMENT_SPLIT to spaced-slash only so Delirium / medical psychosis links while alcohol/benzo, DVT/PE, food/fluid stay intact. Dispositioned: Copilot termLinks ??{} + Fragment key already fixed; CodeRabbit clean-keys moot (visibleSectionItems already cleans); CodeRabbit bare-slash split rejected (clinical harm). No Bugbot findings. Threads cleared on push. Merge left to user. | vitest differential-diagnosis-links+detail+route 49/49; verify:cheap exit 0 (543 files, 5828 passed/4 skipped); verify:pr-local exit 0 (lint/typecheck/test/build/rag-fixtures); merge-tree clean vs origin/main; no provider gates |
| 2026-08-09 | cursor/differentials-diagnosis-links-9f18 | f784e81bcc0b53ef76b3da07a8e81f96d9bf0c71 | pr-1768 unblock | merged origin/main onto ba590f9; merge-tree clean; DIRTY mergeability cleared; push tip follows amend with this ledger | merge-tree clean; threads resolved; auto-merge was armed |
-| 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/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 | 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/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) |
-| 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 | 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 | 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) |
| 2026-08-09 | cursor/differentials-four-page-nav-5ebf | 93ea437610c1f1b681c3a5cbdc72fe8b9b178710 | differentials four-page nav | implemented Search/Diagnoses/Presentations/Compare equal pages; compare queue; kind labels; Search q+run restore | vitest nav+differentials-navigation; typecheck; lint; full unit 5814 passed |
| 2026-08-09 | cursor/differentials-four-page-nav-5ebf | 384a1bedd8dd1064fb2fcf26ac845224e2cafdc4 | PR #1774 differentials four-page nav heavy review-and-fix | fixed P1 ids+Playwright; ModeNav route gate; RSC queue clears bundle+shadow; Copilot ModeNav-on-detail dispositioned (info page); ledger reorder dispositioned (merge=ledger) | vitest nav 47p; design-system-contract; typecheck; lint; test 5897p; build+bundle-budget 1543.7 within tol; focused pw compare queue 1p |
| 2026-08-09 | claude/m3-token-debt-262-261 | 95221ef4235abd9544158b07b8b8569f00c9ec78 | 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 | 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/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 | 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/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 |
diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json
index 8e7729860b..94a1f18f25 100644
--- a/docs/design-system/adoption-manifest.json
+++ b/docs/design-system/adoption-manifest.json
@@ -392,6 +392,7 @@
"tests/answer-render-policy.test.ts",
"tests/citations.test.ts",
"tests/design-sync-visual-exports.test.ts",
+ "tests/document-viewer-shell.dom.test.tsx",
"tests/rag-claim-support.test.ts",
"tests/rag-eval-source-governance.test.ts",
"tests/source-backed-recovery-cross-reference.test.ts",
diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx
index a0aca346d6..c8b41cf574 100644
--- a/src/components/DocumentViewer.tsx
+++ b/src/components/DocumentViewer.tsx
@@ -970,15 +970,6 @@ export function DocumentViewer({
: typeof indexHealth?.warnings === "string" && indexHealth.warnings
? [indexHealth.warnings]
: [];
- useEffect(() => {
- if (!activeChunkId || loadingDocument) return;
- // Citation landing: keep the PDF as the first reading surface. Do not dump
- // the viewport into the indexed source-passages list.
- globalThis.document.getElementById("pdf-preview-section")?.scrollIntoView({
- block: "start",
- behavior: resolveScrollBehavior(),
- });
- }, [activeChunkId, loadingDocument]);
const inspectIndexedTextSection = useCallback(() => {
setInspectRevealKey(`${documentId}::${activeChunkId ?? ""}`);
window.requestAnimationFrame(() => {
diff --git a/src/components/dsm/dsm-compare-remove-link.tsx b/src/components/dsm/dsm-compare-remove-link.tsx
index 252d710af6..b26294e29f 100644
--- a/src/components/dsm/dsm-compare-remove-link.tsx
+++ b/src/components/dsm/dsm-compare-remove-link.tsx
@@ -1,7 +1,6 @@
"use client";
import Link from "next/link";
-import { useRouter } from "next/navigation";
import type { ReactNode, MouseEvent } from "react";
/**
@@ -9,8 +8,10 @@ import type { ReactNode, MouseEvent } from "react";
*
* `` soft-nav from `/dsm/compare?ids=a,b` → `/dsm/compare?ids=b` can
* complete its click handler under Production UI load without ever updating
- * the URL (zero network, URL stuck). Explicit `router.push` is the same
- * pattern `useResultSort` uses for pathname-stable query updates.
+ * the URL (zero network, URL stuck). `router.push` was the next attempt and
+ * still stalled under full-suite Chromium load on PR #1782: the remove
+ * control went `[active]` while `?ids=` stayed unchanged for 15s. A full
+ * assign is the durable hop for this filter change.
*/
export function DsmCompareRemoveLink({
href,
@@ -23,11 +24,10 @@ export function DsmCompareRemoveLink({
className?: string;
children: ReactNode;
}) {
- const router = useRouter();
-
function onClick(event: MouseEvent) {
event.preventDefault();
- router.push(href);
+ event.stopPropagation();
+ window.location.assign(href);
}
return (
diff --git a/tests/docs-inventory.test.ts b/tests/docs-inventory.test.ts
index 4364125cdf..cde7996407 100644
--- a/tests/docs-inventory.test.ts
+++ b/tests/docs-inventory.test.ts
@@ -54,6 +54,9 @@ describe("scripts documentation inventory", () => {
expect(hook).toContain('git cat-file -e "HEAD:scripts/update-docs-inventory.mjs"');
expect(hook).toContain("was removed or renamed by the pending commit; refusing to skip");
expect(hook).toContain("npm run docs:check-index");
+ expect(hook).toContain("npm run design-system:adoption:update");
+ expect(hook).toContain("sync_design_system_adoption=0");
+ expect(hook).toContain("docs/design-system/adoption-manifest.json");
expect(hook).toContain("sync_sitemap=0");
expect(hook).toContain("--diff-filter=ACMRD");
expect(hook).toContain("git ls-files --others --exclude-standard");
diff --git a/tests/document-viewer-shell.dom.test.tsx b/tests/document-viewer-shell.dom.test.tsx
index c1b6c2da9b..7213b0c52c 100644
--- a/tests/document-viewer-shell.dom.test.tsx
+++ b/tests/document-viewer-shell.dom.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// DocumentViewer resolves a four-way shell state (loading / ready / auth-required
@@ -65,7 +65,7 @@ vi.mock("@/components/document-viewer/pdf-canvas-viewer", () => ({
import { DocumentViewer } from "@/components/DocumentViewer";
import type { DocumentDetailPayload } from "@/lib/document-detail-contract";
-function detailPayload() {
+function detailPayload(): DocumentDetailPayload {
return {
document: {
id: "doc-1",
@@ -101,7 +101,7 @@ function detailPayload() {
},
pageWindow: { from: 1, to: 4, limit: 4, total: 4, hasBefore: false, hasAfter: false },
chunkWindow: { offset: 0, limit: 8, total: 8, hasBefore: false, hasAfter: false, selectedChunkId: null },
- } satisfies DocumentDetailPayload;
+ };
}
// In demo / local-no-auth mode every document is public, so the private-access
@@ -173,6 +173,45 @@ describe("DocumentViewer — shell states", () => {
expect(screen.queryByText("Sign in required")).toBeNull();
});
+ // Search/answer opens always attach ?chunk=…. Citation landing used to
+ // scrollIntoView(#pdf-preview-section) whenever a chunk was present, which
+ // skipped the phone overview at the top. Open at the top; the PDF still
+ // targets the cited page inside its own canvas.
+ it("does not auto-scroll the page to the PDF when opening with a chunk deep-link", async () => {
+ const scrolledIds: string[] = [];
+ vi.mocked(Element.prototype.scrollIntoView).mockImplementation(function scrollIntoView(this: Element) {
+ if (this.id) scrolledIds.push(this.id);
+ });
+
+ const detail = detailPayload();
+ detail.chunks = [
+ {
+ id: "chunk-1",
+ page_number: 1,
+ chunk_index: 0,
+ section_heading: "Scope",
+ content: "Cited passage",
+ image_ids: [],
+ metadata: {},
+ },
+ ];
+ detail.window.selectedChunkId = "chunk-1";
+ detail.window.chunks.selectedChunkId = "chunk-1";
+ detail.chunkWindow.selectedChunkId = "chunk-1";
+
+ render();
+
+ expect(await screen.findByRole("heading", { level: 1, name: "Clozapine Titration Guideline" })).toBeVisible();
+ expect(document.getElementById("pdf-preview-section")).not.toBeNull();
+
+ // Flush mount effects before the negative assertion. A waitFor that can
+ // pass while scrolledIds is still empty would miss a late scrollIntoView.
+ await act(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+ expect(scrolledIds).not.toContain("pdf-preview-section");
+ });
+
it("requires two characters and ignores an aborted search response after the query changes", async () => {
const pendingSearches: Array<{
url: string;
diff --git a/tests/dsm-compare-remove-link.dom.test.tsx b/tests/dsm-compare-remove-link.dom.test.tsx
new file mode 100644
index 0000000000..6fd4715116
--- /dev/null
+++ b/tests/dsm-compare-remove-link.dom.test.tsx
@@ -0,0 +1,30 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { DsmCompareRemoveLink } from "@/components/dsm/dsm-compare-remove-link";
+
+describe("DsmCompareRemoveLink", () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+ });
+
+ it("assigns the href on click so same-route search-param removals cannot stall", () => {
+ const assign = vi.fn();
+ vi.stubGlobal("location", { ...window.location, assign });
+
+ render(
+
+ Remove
+ ,
+ );
+
+ const link = screen.getByRole("link", { name: "Remove Major depressive disorder from comparison" });
+ fireEvent.click(link);
+
+ expect(assign).toHaveBeenCalledWith("/dsm/compare?ids=bipolar-ii-disorder");
+ });
+});
diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts
index 277b3a7549..0d76b29314 100644
--- a/tests/ui-route-coverage.spec.ts
+++ b/tests/ui-route-coverage.spec.ts
@@ -307,16 +307,19 @@ test.describe("previously uncovered production routes", () => {
async (currentPage) => {
// Scope to the visible comparison owner (#093): under Production UI load,
// Next streaming can leave a hidden duplicate root, and same-route
- // search-param `` soft-nav has been observed to click without
- // updating the URL. Pin the visible tree and wait for navigation with
- // the click (DsmCompareRemoveLink uses router.push for this hop).
+ // search-param soft-nav has been observed to click without updating the
+ // URL. Pin the visible tree and wait for navigation with the click
+ // (DsmCompareRemoveLink uses location.assign for this hop).
const pageRoot = visibleByTestId(currentPage, "dsm-comparison-page");
const remove = pageRoot.getByRole("link", {
name: "Remove Major depressive disorder from comparison",
});
await expect(remove).toBeEnabled();
await Promise.all([
- currentPage.waitForURL(/\/dsm\/compare\?ids=bipolar-ii-disorder$/, { timeout: 15_000 }),
+ currentPage.waitForURL(/\/dsm\/compare\?ids=bipolar-ii-disorder$/, {
+ timeout: 15_000,
+ waitUntil: "domcontentloaded",
+ }),
remove.click(),
]);
await expect(currentPage.getByRole("heading", { name: "Choose at least two diagnoses" })).toBeVisible();