From 01825c09b95cef89893d33f569b6967555b19d6b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 22:25:17 +0000 Subject: [PATCH 1/6] issues: add #286 to recommended queue (A2, authorizationHeader naming helper) Resolves the Codex P2 review finding on PR #1754: the authorizationHeader lowercase trap was recorded as issue #286 in the open items but not added to the recommended execution queue, so session-start and /issues summaries would not schedule it. Added as queue position 113 at A2 level, scoped to the next auth module touch. --- docs/outstanding-issues.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index de36b520dc..c2d24454a7 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -163,6 +163,7 @@ removed after current-main verification; it is not missing recommended work. | 110 | `#255` | A2 | High — Cloud/browser gates | Next environment image update | 2–4 hours | Align Cloud Playwright browser builds with lockfile pin; document CI delegation until then. **Stop:** do not force mismatched Chromium revisions. | | 111 | `#256` | A2 | High — mode section nav | Next information-page / mode-nav pass | 2–4 hours | Declared information-page section sets whose target ids nothing renders — verify each set against the rendered DOM per route; render anchors or delete the set. **Stop:** do not audit by grepping for `id=` alone (sectionId props exist). | | 112 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | +| 113 | `#286` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | From b8c94dff2345a7d50c7bbce0c9c344740e6b92b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 01:43:34 +0000 Subject: [PATCH 2/6] docs(document-viewer): add the Phase 3 execution handover Phases 0-2 of the viewer redesign merged as PR #1741; Phase 3 was never started, so the viewer still rasters one page at a time and every page flip on a long guideline is a cold render. This brief scopes Phase 3 to every capability except crop -> page overlay, which is excluded because `bbox` is absent from `DocumentDetailImage` and plumbing it crosses into `src/lib/**document**` -- a path that trips `clinicalRiskPatterns` and forces a governance preflight. Toolbar density is recorded as already shipped in Phase 2. It also corrects ledger #279, which claimed the viewer canvas cannot be gated in a browser and proposed bumping Playwright or pinning `pdfjs-dist` down. Measured: pinned `playwright@1.62.1` expects Chromium 151.0.7922.34, this container ships 141.0.7390.37, and CI runs HeadlessChrome/151.0.0.0. `pdfjs-dist@6.2.108` calls `Map.prototype.getOrInsertComputed`, which ships in 151 and not 141, so the failure is container-only and neither remedy is needed. All line references verified against main at 8db1e53. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- docs/plans/document-viewer-phase3-handover.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/plans/document-viewer-phase3-handover.md diff --git a/docs/plans/document-viewer-phase3-handover.md b/docs/plans/document-viewer-phase3-handover.md new file mode 100644 index 0000000000..07dea84e9d --- /dev/null +++ b/docs/plans/document-viewer-phase3-handover.md @@ -0,0 +1,160 @@ +# Document viewer — Phase 3 handover + +Execution brief for Phase 3 of `docs/plans/document-viewer-redesign-plan.md`. Phases 0–2 merged as +PR #1741 (squash `42f87ca`); Phase 3 was never started. The viewer still rasters exactly one page at a +time, so every page flip on a long guideline is a cold render — that is the remaining felt slowness. + +**Scope:** every Phase 3 capability **except crop → page overlay**. Crop overlay is deliberately out — +`bbox` is SELECTed at `src/lib/document-detail.ts:441` but absent from `DocumentDetailImage` in +`src/lib/document-detail-contract.ts`, so it needs contract plumbing through `src/lib/**document**`, +which trips `clinicalRiskPatterns` in `scripts/pr-policy.mjs` and forces a governance preflight. Every +remaining item stays inside `src/components/document-viewer/**`, which classifies `clinicalRisk: false`. + +**Already done:** toolbar density shipped in Phase 2 (`document-frame.tsx:404`, `hidden sm:inline` plus +an `sm:hidden` overflow menu). Strike it from the plan's table. + +All line references below were verified against `main` at `8db1e53`. + +--- + +## Task 0 — establish the canvas gate first (prerequisite) + +Ledger `#279` says the viewer canvas cannot be gated in a browser, and proposes either bumping the +pinned Playwright build or pinning `pdfjs-dist` down. **That diagnosis is wrong and both remedies would +be wasted dependency surgery.** Confirm and correct it before writing any feature code: + +```bash +node -e "const b=require('$PWD/node_modules/playwright-core/browsers.json'); \ + b.browsers.filter(x=>x.name==='chromium').forEach(x=>console.log(x.revision, x.browserVersion))" +/opt/pw-browsers/chromium-*/chrome-linux/chrome --version +``` + +Measured 2026-08-09: + +| | value | +| ---------------------------------------------------------------------- | ---------------------------- | +| Container Chromium (`/opt/pw-browsers/chromium-1194`) | 141.0.7390.37 | +| Chromium pinned `playwright@1.62.1` expects (`browsers.json` rev 1234) | **151.0.7922.34** | +| Chromium CI actually runs (`lighthouse-budget.json:27`) | **HeadlessChrome/151.0.0.0** | +| `pdfjs-dist@6.2.108` calls `Map.prototype.getOrInsertComputed` | `pdf.mjs:2454, 6889, 6896` | + +`getOrInsertComputed` ships in Chromium 151 and not in 141, so the raster failure is +**container-only** — CI's browser already runs pdf.js 6 fine. The container's pre-installed browser is +simply older than its own pinned Playwright wants, and `PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers` pins +it there. Do not bump Playwright, do not pin `pdfjs-dist` down, and do not run `playwright install` +(this environment forbids it). + +Write a Playwright viewer-canvas journey asserting a page actually paints: non-blank canvas pixels, +correct page count, page-flip changes the raster. Expect it to fail locally with +`getOrInsertComputed is not a function` and pass in CI. Guard it so the local failure is an explicit +skip-with-reason, never a silent green — `docs/testing.md` flake policy applies. Then supersede `#279` +with `npm run ledger:append -- --supersede`, recording it as container-only with the measured numbers. + +Everything below is proven by that gate in CI plus focused unit tests locally. + +## Task 1 — multi-page virtualization (the core item) + +`src/components/document-viewer/pdf-canvas-viewer.tsx` (524 lines) renders one page into one ``. +Move to a windowed list: render near pages, dispose far canvases, keep page ↔ URL sync intact. + +Four constraints that will bite, all load-bearing: + +1. **The raster budget becomes document-wide.** `resolveCanvasRasterPlan` (`canvas-raster-budget.ts`) + bounds _one_ canvas to 2^24 device pixels because WebKit paints nothing above it. With N live + canvases, N individually-legal pages can still exhaust device memory. Add a document-wide live-pixel + budget capping how many rendered canvases are retained. Do not raise `MAX_CANVAS_PIXELS`. +2. **Render-ahead fights `disableAutoFetch`.** `getDocument({ disableAutoFetch: true, disableStream: +true })` at `:189` was chosen precisely because a reader looks at one page — it stops pdf.js pulling + a whole guideline over cellular. Pre-rendering neighbours re-introduces exactly that fetch + amplification. Resolve it deliberately: ±1 page, on idle, and state the trade in the PR body. Do not + silently drop `disableAutoFetch`. +3. **Page-sync feedback loop.** The effect at `:216-229` already does rAF → clamp → reconcile the parent + route via `onPageChangeRef`. If "current page" becomes a scroll derivation, that effect can fight the + scroll position. Derive the active page from scroll, but let only user intent write the URL. +4. **Keep the `memo` boundary.** `PdfCanvasViewer` is memoised (`:53`, rationale at `:45-52`) so a + keystroke in the composer never re-rasters. Per-page state must not lift into the parent and defeat + that. + +Preserve: the per-run `pageToCleanup` isolation (Sentry 15801413), the canvas zeroing on unmount +(`:337-345`), the `renderZoom` debounce with its interim CSS transform, and the `isLikelyExpiredUrl` → +`reportUrlExpired` recovery path — which must still work when a range request for a _neighbour_ page is +the thing that 403s. + +## Task 2 — rail virtualization + +`document-rail-panels.tsx:223` (`id="source-images"`) maps `clinicalImages` and `auditImages` into +`DocumentImage` rows. `auditImages` sit inside a collapsed `
` but are still in the DOM and +still mint signed URLs. Virtualize the long list and stop off-screen rows resolving URLs. + +## Task 3 — signed-URL and decode priority + +Above-fold evidence should resolve and decode before the below-fold rail. Build on the in-flight dedupe +already in `use-signed-image-url.ts` — **read the warning below before touching that file.** +`src/app/api/images/signed-urls/route.ts` batches up to 100 ids and has no caller (ledger `#283`); +wiring it is in scope if the rail mounts several distinct images at once, but keep the per-image +endpoint for the lightbox retry path. + +## Task 4 — keyboard reading mode + +`handleHolderKeyDown` (`:410-441`) already handles ArrowLeft/Right, `+`/`=`, `-`, and `0`. Phase 3 adds +PageUp/PageDown, `f` (fit), and `r` (rotate). `rotation` is an inbound prop with **no** +`onRotationChange` callback — `r` needs a new prop threaded from `DocumentFrame`. Document the bindings +and test them. + +## Task 5 — OffscreenCanvas: measure, then decide + +The plan conditions this on "measured main-thread paint cost." Measure first and report the number. If +virtualization already lands the win, say so and skip it — do not implement it on principle. + +--- + +## Verification + +```bash +npm run test:focused -- --files \ + tests/use-viewer-gestures.dom.test.tsx,tests/document-viewer-shell.dom.test.tsx,\ +tests/document-viewer-pdf-reader-lazy.test.ts,tests/document-frame-contract.test.ts,\ +tests/document-detail-performance.test.ts,tests/client-performance-boundaries.test.ts + +npm run ensure # before any browser work; never assume a port +npm run verify:phone-chrome -- --dry-run +npm run format # AND COMMIT IT — not in verify:cheap, blocks CI +npm run verify:pr-local +``` + +Two gates will move and must not be silenced: + +- **`check:bundle-budget`** totals _every_ built chunk against 1,440,201 gzip bytes at 10% tolerance + (`bundle-budget.json`). A virtualization dependency would land straight on it — prefer none. +- **The `document-viewer` visual golden will shift.** Per ledger `#278` that target composites + viewport-pinned chrome over content, and its position tracks total content height, so any + content-height change inflates the diff. Expect it; fix it by narrowing the clip or masking the + pinned chrome. `#278` records that capturing `fullPage` is banned (ledger `#093`). + +## Do not + +- Do not weaken a viewer assertion to make it pass in this container. +- Do not re-gate pinch on `!fitWidth` (ledger `#280` — that restores the original defect). +- Do not touch `src/lib/rag/**` or any ranking surface; nothing here should. +- Do not run provider-backed gates (`verify:release`, `eval:*`, `check:supabase-project`) without asking. + +## Read this before editing `use-signed-image-url.ts` + +The Phase 0–2 pass shipped two identity bugs in that file, both caught in review. +`authorizationHeadersForAccessToken` emits **lowercase** `authorization` +(`src/lib/supabase/client.tsx:82`), but the dedupe key read `headers.Authorization` — so the token was +never in the key, every identity collapsed onto `endpoint `, and an account switch with a request in +flight could hand user B user A's signed URL. The second: the module LRU was written from inside the +shared promise, so a superseded response could refill a cleared cache after an identity change. +`authorizationHeader` is a `Record`, so reading the wrong casing fails silently. Ledger +`#286` tracks the casing helper that would make this unrepeatable — consider landing it first. + +## Handoff + +Stage as separate commits per task so any one stays independently revertible while the PR is open. The +PR body needs no `## Clinical Governance Preflight` and no `RAG impact:` line **provided** the diff +stays inside `src/components/document-viewer/**` and `tests/**` — if it reaches `src/lib/**document**` +or `src/app/api/**`, both become required. Check with +`npm run verify:pr-local -- --dry-run --files `. + +Capture anything deferred with `/issues capture` before the session ends. From a7fd780ff1b0aa1aa914f582e93161f6da59a62d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 01:46:24 +0000 Subject: [PATCH 3/6] docs(ledger): record the Phase 3 handover brief review for PR #1765 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1a2b3fc30a..66e8f46c10 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -807,3 +807,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | e965d96258cdd3d5ad6f520616de0f32bf02498e | Form 1A priority facts: condense cards + Act section detail sheets | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | 7040b850e655dc7ba9a23ad3e3db765cc1ad7755 | Form 1A priority facts: condense cards + Act section detail sheets | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | | 2026-08-08 | cursor/form-1a-priority-facts-dc36 | e965d96258cdd3d5ad6f520616de0f32bf02498e | Form 1A priority facts: condense cards + Act section detail sheets (supersedes 2026-08-08) | implemented; Form 1A Source status card replaced with Act sections 26/31/36/37/41/42; condensed clock/maker/criteria with tap sheets | typecheck pass; lint pass; npm run test 530 files / 5704 passed | +| 2026-08-09 | claude/document-viewer-optimization-tu8tnj | b8c94dff2345a7d50c7bbce0c9c344740e6b92b1 | docs: document-viewer Phase 3 handover brief (PR #1765) | Docs-only. Adds docs/plans/document-viewer-phase3-handover.md scoping Phase 3 to all capabilities except crop-to-page overlay (bbox absent from DocumentDetailImage; plumbing crosses src/lib/**document** and forces a governance preflight). Corrects ledger #279: measured playwright@1.62.1 expects Chromium 151.0.7922.34, container ships 141.0.7390.37, CI runs HeadlessChrome/151.0.0.0, and pdfjs-dist 6.2.108 needs Map.getOrInsertComputed which ships in 151 not 141 - so the raster failure is container-only and neither proposed remedy (bump Playwright / pin pdfjs down) is needed. Cited #286 for the authorizationHeader casing trap after initially writing #285. | verify:pr-local all ten gates completed, none failed; docs:check-links 1688 references resolve; line refs re-verified against main 8db1e53 | From 18257ad8d220e88a7ccd153e46632c25f448f7fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 01:56:39 +0000 Subject: [PATCH 4/6] docs(ledger): supersede the PR #1765 row with correct skipped-check scope Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 5b91d29a39..17f12774aa 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -825,3 +825,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-07 | cursor/site-testing-speed-08c1 | 91bac89827ae2f4f0e59aeed7de6344fe8779a95 | PR #1686 Autopilot+Bugbot review-and-fix: conflicts, threads, Static PR checks, CI/testing selection | fixed: merged origin/main (outstanding-issues #167/#255 archive + #256 keep); removed unused pathToFileURL; added ui-forms-section-nav to PR UI shards (21 specs); no unresolved threads; Bugbot unavailable (usage limit). Local: eslint file max-warnings0, vitest 36/36 focused, shard --validate OK, check:outstanding-issues OK. verify:cheap/pr-local blocked by foreign worktree heavy lock (PID 26228). | eslint scripts/playwright-pr-shards.mjs --max-warnings 0; vitest 36 passed; playwright-pr-shards --validate 21; check:outstanding-issues; verify:cheap/pr-local lock-blocked | | 2026-08-08 | cursor/more-modes-popup-2f4b | 04e6a80653c3ccf103577f6c3886b162498621ed | sidebar more-modes sheet popup | pass | focused-pw tablet rail; test:focused ClinicalSidebar; favourites+therapy wiring; verify:pr-local stages+build+rag-fixtures | | 2026-08-08 | cursor/more-modes-popup-2f4b | bea4b0c09b74368cf6d63e944bac9c1eec6b0c93 | sidebar more-modes sheet popup | pass | focused-pw tablet rail; test:focused ClinicalSidebar; favourites+therapy wiring; verify:pr-local stages+build+rag-fixtures | +| 2026-08-09 | claude/document-viewer-optimization-tu8tnj | 5a0d6be02bc92fa2615d2141b338ec8f7c1143b1 | docs: document-viewer Phase 3 handover brief (PR #1765) | Supersedes the earlier row, whose 'all ten gates completed' wording could read as all executable checks having run. Correct scope: verify:pr-local ran the ten gates APPLICABLE to docs-only changes (check:runtime, check:installed-lock-parity, format:changed, sitemap:check, docs:check-index, docs:check-inventory, docs:check-scripts, docs:check-links, check:branch-review-ledger, check:outstanding-issues); the risk router SKIPPED lint, typecheck, the full unit suite, RAG fixture validation, and build as recognised low-risk documentation scope. Also records the merge resolution: duplicate #286 (main's in-page-nav series vs this branch's authorizationHeader row) resolved by renumbering the branch row to #289, next-id 290, after the auto-merge silently dropped that detail row rather than conflicting. Review findings addressed: governance preflight now required by behaviour per AGENTS.md:257 rather than inferred from pr-policy path classification; API-route scope contradiction resolved; signed-URL warning corrected to state both identity bugs are already fixed on main with regression coverage. | verify:pr-local ten docs-scope gates passed, none failed; check:outstanding-issues 287 rows unique ids next-id=290 no ids deleted; ledger:dedupe 771 unique rows; git merge-tree vs origin/main exit 0; viewer line refs re-verified against 50ef12e | From 4f5a2564111c1105ebefb8fb805e9f82fe856ffb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 02:03:50 +0000 Subject: [PATCH 5/6] docs: correct the #279 supersede instruction and update the durable row The brief told the next session to close #279 with `ledger:append --supersede`. That is the wrong tool and the wrong file: #279 lives in docs/outstanding-issues.md, so it closes through `issues:done` / `issues:update`, while `ledger:append` writes the separate branch-review ledger and would have left the durable row untouched. Also updates #279 itself, which still carried the refuted remedy (bump the pinned Playwright build, or pin pdfjs-dist down). Neither is needed: pinned playwright@1.62.1 expects Chromium 151.0.7922.34, this container ships 141.0.7390.37, and CI runs HeadlessChrome/151.0.0.0. pdfjs-dist 6.2.108 calls Map.prototype.getOrInsertComputed, which ships in 151 and not 141, so the raster failure is container-only. Recording that on the row stops any session acting on the refuted fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- docs/outstanding-issues.md | 2 +- docs/plans/document-viewer-phase3-handover.md | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 1ccb6b549d..e945a70fe1 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -319,7 +319,7 @@ removed after current-main verification; it is not missing recommended work. | #275 | P2 | task | The shared filter trigger carries arbitrary spacing values inherited from DocumentFilterTrigger | **Outcome:** the phone filter trigger expresses its measurements as named tokens rather than bracketed values. **Detail:** `result-filter-control.tsx`'s `ResultFilterTrigger` uses `pr-[0.6875rem]`, `h-[1.0625rem]`/`min-w-[1.0625rem]` for the badge, and the raw breakpoint window `min-[414px]:max-[429px]` for the label. CodeRabbit flagged these against the design-token rule in PR #1706. Every one of them is copied *verbatim* from `DocumentFilterTrigger`, which shipped on main earlier and is the component this one was deliberately lifted from so the two cannot drift — so the finding is real but its scope is both call sites, not the new one. Changing only the copy would reintroduce exactly the drift the extraction removed, and each value carries a measured justification in its own comment (the asymmetric padding answers a stroked glyph against a filled pill; the breakpoint window is the one band that is single-line and short of width). **Next:** tokenise in `@theme` once, then update the trigger — there is now only one implementation, so it is a single edit. Confirm the badge and padding render identically at 393/402/414/430px before and after. **Stop:** do not tokenise the trigger without also retiring the values from the documents original, and do not treat this as licence to change the measurements themselves. | CodeRabbit review on PR #1706; DocumentFilterTrigger on main | 2026-08-07 | | #277 | P2 | issue | docs/design-system/HANDOVER-2026-08-07.md is cited as provenance by nine ledger rows but is measurably wrong | Outcome: no session scopes design-system work from a document whose figures have already been disproved. Evidence: rows #261, #262, #264, #265, #266, #267, #268, #269 and #270 all carry 'session 2026-08-07 — design-system HANDOVER-2026-08-07 Track A1 handoff (PR #1678)' as their Source, and docs/design-system/README.md links it as 'measured state, the ordered plan'. Measured wrong so far, all corrected into the rows themselves rather than the document: its '229 --shadow-tight aliases' is the seven-token legacyShadowAliases total mislabelled as one token (real figure 100 sites across 55 files, total 228); its adoption figure was 24 unadopted against a measured 23; its claim that visual baselines cannot be generated on Windows is half true and led to the wrong conclusion, since the ubuntu CI job already produces the ones that count; and #270's '22 call sites pair a tap token with a dead numeric height' does not survive re-measurement at all (zero same-variant pairs, 84 cross-variant responsive step-downs that are not dead). The document itself still asserts the originals. Next: cheapest fix is a superseded banner at the top naming the ledger rows as the current source of truth, plus the same in docs/design-system/README.md's link text — not a rewrite, because the corrections already live in the rows and duplicating them re-creates the drift. If the live path should leave docs/design-system/, move the file to docs/archive/ (or the design-system archive) and update inbound links per docs/README.md; do not delete it, because the nine Source citations, the PR/commit record, and the handover's verification/gotcha sections are provenance the ledger is meant to preserve. Stop: do not re-copy its figures into any new plan or handover, do not delete the evidence, and do not silently correct it in place, which would leave the nine Source citations pointing at a document that no longer says what those rows were derived from. | session 2026-08-08 — measured while closing #263 follow-ups across PRs #1719 and #1720 | 2026-08-08 | | #278 | P3 | issue | The document-viewer visual baseline bakes in viewport-pinned chrome that overlaps content | Measured 2026-08-08 while adopting the baselines (#118 / PR #1729). The document-viewer target clips #main-content, which is 1196x2903 against a 900px viewport, and contains viewport-pinned chrome: the sm:sticky sm:top-0 document header (DocumentViewer.tsx:1028) and the sm:fixed search composer (DocumentViewer.tsx:1511). Playwright stitches an oversized element clip, so both composite partway down the image and OVERLAP the content behind them — the cited-excerpt card and a source passage are partly covered in the committed golden. Position tracks total content height, so any content-height change above them moves the pinned chrome and inflates the diff well beyond what actually changed. NOT a product bug and NOT a #1705 regression: the pre-#1705 candidate from run 31249978408 shows the same overlap, so it is inherent to the target's design. The capture is deterministic, so the comparison still means something — five of six candidates were byte-identical by SHA-256 across two independent CI runs. Next: narrow that target's clip to a smaller locator, or add the pinned chrome to the target's mask array (the spec already supports mask, with a comment warning a mask is a hole in the gate). Stop: do not fix this by capturing fullPage — the spec bans it because ledger #093 leaves a hidden duplicate page root under CI load. | session 2026-08-08 — visual baseline adoption, #118 | 2026-08-08 | -| #279 | P2 | issue | pdf.js 6 cannot raster in this container's Chromium, so no browser gate covers the viewer canvas | **Outcome:** viewer canvas behaviour is provable by a gate rather than only by unit test and device. **Detail:** the document route renders 'this[#methodPromises].getOrInsertComputed is not a function' instead of a page in Chromium 141.0.7390.37 at /opt/pw-browsers — pdfjs-dist 6.2.108 calls a Map builtin that shipped after 141. Reproduced identically on bc33d41 and on the viewer-optimization branch, so it is the environment, not a regression. Consequence: every Playwright assertion that depends on a painted canvas is unprovable here, which covers the canvas pixel budget and the fit-mode pinch gesture landed in this branch, and any future 'verify:ui' viewer journey run in this container or a like-configured cloud session. **Next:** confirm whether CI's Chromium is newer than 141 (if so this is container-only and should be recorded as such); otherwise either bump the pinned Playwright browser build or pin pdfjs-dist to a release whose baseline the gate's browser meets. **Stop:** do not weaken a viewer assertion to make it pass in this container. | session 2026-08-08 document-viewer optimisation; /opt/pw-browsers/chromium-1194 = Chromium 141.0.7390.37 | 2026-08-08 | +| #279 | P2 | issue | pdf.js 6 cannot raster in this container's Chromium, so no browser gate covers the viewer canvas | **Outcome:** viewer canvas behaviour is provable by a gate rather than only by unit test and device. **CORRECTED 2026-08-09 (PR #1765) — the original remedies were wrong; do not action them.** Measured: pinned playwright@1.62.1 expects Chromium 151.0.7922.34 (playwright-core/browsers.json rev 1234); this container ships 141.0.7390.37 at /opt/pw-browsers/chromium-1194; CI runs HeadlessChrome/151.0.0.0 (recorded in lighthouse-budget.json:27). pdfjs-dist@6.2.108 calls Map.prototype.getOrInsertComputed (pdf.mjs:2454, 6889, 6896), which ships in Chromium 151 and not 141. So 'this[#methodPromises].getOrInsertComputed is not a function' is CONTAINER-ONLY: CI's browser already runs pdf.js 6 correctly. Do NOT bump the pinned Playwright build and do NOT pin pdfjs-dist down — the container's pre-installed browser is simply older than its own pinned Playwright expects, and PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers pins it there. This environment also forbids running 'playwright install'. **Next:** write the viewer-canvas Playwright journey (non-blank canvas pixels, page count, page-flip changes the raster), let it skip-with-reason locally and prove in CI; this is Task 0 of docs/plans/document-viewer-phase3-handover.md. **Stop:** do not weaken a viewer assertion to make it pass in this container. | session 2026-08-09 Phase 3 handover, PR #1765; playwright-core/browsers.json rev 1234; lighthouse-budget.json:27 | 2026-08-08 | | #280 | P2 | task | Physical iPhone acceptance is owed for the viewer pinch gesture and the canvas pixel budget | **Outcome:** the two phone-only viewer fixes are confirmed on the device class they were written for. **Detail:** the viewer-optimisation branch revives pinch-to-zoom in fit mode (it was gated off in the default state, so a pinch reached neither the viewer nor the browser) and adds a canvas pixel budget so WebKit stops blanking the page above roughly 2.3x zoom on a dpr-3 display. Neither is verifiable in this container (see the Chromium/pdf.js row) and neither is a Chromium behaviour anyway — the canvas ceiling is a WebKit limit and the touch-action contention is a Safari gesture question. **Next:** on a real iPhone, in Safari and in the installed PWA: pinch a freshly opened document and confirm it zooms without first tapping a control; zoom to maximum and confirm the page stays painted rather than going blank; confirm a pinch that drifts vertically is not cancelled mid-gesture by the holder's 'touch-action: pan-y' (the mitigation if it is, is switching touch-action to none while two pointers are down — the gesture hook already tracks pointer count and exposes 'pinching'). Record the result against docs/phone-chrome-physical-acceptance.md. **Stop:** do not re-gate pinch on '!fitWidth' to resolve a gesture-contention finding — that restores the original defect. | session 2026-08-08 document-viewer optimisation; docs/design-system/COMPONENTS.md phone clause | 2026-08-08 | | #281 | P2 | rec | The phone document route renders two clinical-summary surfaces and neither is canonical | **Outcome:** one clinical summary on the document route, chosen deliberately. **Detail:** a phone reader gets the gradient 'High-yield clinical summary' card (DocumentClinicalSummary, built by buildDocumentClinicalSummaryModel) and, further down, the rail's '#source-summary' / 'high-yield-summary' disclosure (DocumentSectionSummary + FormattedHighYieldSummary + BadgeCluster). They render the same document.summary row two different ways. The rail is not hidden on phones — only its DocumentSectionIndexCard is lg:block — so both appear. Only the rail panel carries the section anchor, so the more prominent card is the unnavigable one. Note the two disagree about emptiness as well: the card now renders nothing when the model yields no usable text, while the rail panel still renders for its label badges, which is why 'hasStoredSummary' was deliberately left keyed to the stored row rather than to card content. **Next:** decide which rendering is canonical — this is a clinical-content judgement about how a summary should read, not a layout fix — then delete the other and give the survivor the 'source-summary' anchor. If the rail's badges are the part worth keeping, they can move without the second summary body. **Stop:** do not merge the two renderings mechanically; they format clinical text differently and the difference is the decision. | session 2026-08-08 document-viewer optimisation; document-rail-panels.tsx; document-clinical-summary.tsx | 2026-08-08 | | #282 | P3 | task | Probe the corpus for JBIG2/JPX before deciding whether pdf.js needs its decoder assets shipped | **Outcome:** a measured decision about pdf.js's cMap/standard-font/WASM assets rather than an assumption either way. **Detail:** getDocument is configured with url plus the on-demand fetch flags and nothing else, so 'wasmUrl', 'standardFontDataUrl', 'cMapUrl' and 'iccUrl' are all unset. pdfjs-dist ships those assets (wasm 1.5 MB, standard_fonts 804 KB, cmaps 1.7 MB) and nothing copies them into public/. With wasmUrl null, 'useWorkerFetch' resolves false and the WASM image decoders cannot load, so JBIG2 and JPEG2000 images fall back to the JS decoders or fail; those are exactly the encodings a scanned guideline uses, and this repo runs an OCR pipeline, which implies scanned sources exist. Non-embedded standard-14 fonts fall back to system fonts, which is a fidelity risk on a clinical document rather than a failure. **Next:** sample the real corpus for JBIG2/JPX-encoded images and for PDFs relying on the standard 14 before shipping ~2 MB of static assets; if the corpus does use them, copy into public/pdfjs, set the URLs, and add immutable cache headers in next.config.ts (public/ is not counted by check:bundle-budget, so there is no budget risk — the cost is bytes over the wire on first use). **Stop:** do not ship the assets on the assumption alone. | session 2026-08-08 document-viewer optimisation; node_modules/pdfjs-dist/types/src/display/api.d.ts | 2026-08-08 | diff --git a/docs/plans/document-viewer-phase3-handover.md b/docs/plans/document-viewer-phase3-handover.md index 86003287d3..ac6fe1335f 100644 --- a/docs/plans/document-viewer-phase3-handover.md +++ b/docs/plans/document-viewer-phase3-handover.md @@ -53,8 +53,12 @@ it there. Do not bump Playwright, do not pin `pdfjs-dist` down, and do not run ` Write a Playwright viewer-canvas journey asserting a page actually paints: non-blank canvas pixels, correct page count, page-flip changes the raster. Expect it to fail locally with `getOrInsertComputed is not a function` and pass in CI. Guard it so the local failure is an explicit -skip-with-reason, never a silent green — `docs/testing.md` flake policy applies. Then supersede `#279` -with `npm run ledger:append -- --supersede`, recording it as container-only with the measured numbers. +skip-with-reason, never a silent green — `docs/testing.md` flake policy applies. + +`#279` lives in `docs/outstanding-issues.md`, so close it with `npm run issues:done -- '#279'` (or +`issues:update` if the gate is only partly built) — **not** `ledger:append --supersede`, which appends to +the separate branch-review ledger and would leave the durable row untouched. Its refuted remedy has +already been struck from the row; record the gate you built as the resolution. Everything below is proven by that gate in CI plus focused unit tests locally. From 20bfa7864fd3087c289c2e853f7589a3b325a9ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 02:05:33 +0000 Subject: [PATCH 6/6] docs(issues): describe the recommended queue as a curated subset The preamble claimed the queue is 'the ordered view of every open ledger row'. It is not: 116 queue rows against 143 open rows, because the queue deliberately omits deprioritised-but-open items. The claim made the queue look lossy when it was working as designed. Caught in PR #1765 review. My first rebuttal was wrong -- I cited the sentence about post-verification removals, which is about a different thing, and missed the completeness claim one line above it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- docs/outstanding-issues.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index e945a70fe1..593ef710d6 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -32,7 +32,9 @@ task status, priority, order, dependencies, and completion state are canonical o ## Recommended execution queue -This queue is the ordered view of **every open ledger row**. Revalidate a row against current +This queue is the ordered, prioritised view of open ledger rows — a **curated subset**, not every +open row. The detailed tables below remain the complete record; absence from this queue means +deprioritised, never closed. Revalidate a row against current `main` before starting. Its position is not authority to call providers, spend money, change production, commit, push, open a PR, or deploy. Acuity still decides what to start first; Optional rows are deferred until their trigger.