Skip to content

fix(access): complete outstanding P0/P1 hardening and test alignment - #279

Closed
BigSimmo wants to merge 7 commits into
cursor/content-access-review-a385from
cursor/access-hardening-f048
Closed

fix(access): complete outstanding P0/P1 hardening and test alignment#279
BigSimmo wants to merge 7 commits into
cursor/content-access-review-a385from
cursor/access-hardening-f048

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Completes outstanding access-hardening work and P2/P3 follow-up verification.

Code fixes (prior commits)

  • Image signed-url route: repair broken merge while preserving owner-check-before-storage + committed-generation gating
  • Setup status: JSON error envelope on unexpected failures
  • Access route tests: managed port 4298, upload guard 409, withOwnerReadScope mock alignment

P2/P3 verification (this turn)

  • npm run governance:releasePASS (label coverage, label governance, source metadata audit)
  • Gold-label backfill dry-run — 0 documents need labels
  • tests/private-access-routes.test.ts + tests/answer-render-policy.test.ts99/99 pass
  • npm run eval:quality:releaseFAIL thresholds: P95 latency ~50.7s (ceiling 25s); 4 latency cases >20s; 2 routing mismatches (illegal-substances, unsupported-close-title-noise)
  • tests/ui-tools.spec.tsblocked by local server port churn (4298 vs 4299) during long Playwright run; needs a single ensured URL for the full suite

Docs

  • docs/clinical-governance.md: records public/anonymous API access patterns, rate-limit buckets, signed-url gates, and upload local-project guard

Follow-up

  • Product decision on eval latency / review thresholds (or infra tuning for answer generation)
  • Re-run verify:ui / ui-tools with stable npm run ensure URL (no competing dev servers)
  • Live DB-level RLS integration test (anon key, two users) — still tracked as follow-up
Open in Web Open in Cursor 

BigSimmo added 6 commits July 5, 2026 22:01
- Hide footer Evidence/Sources chips on phone hero composers; scope stays in + menu
- Suppress bottom-dock composer flash until hero portal slot is ready
- Increase composer action/send touch targets to 44px on phones
- Update Playwright tests for scope menu, Answer home geometry, and stress fallback
@supabase

supabase Bot commented Jul 5, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@BigSimmo
BigSimmo marked this pull request as ready for review July 5, 2026 17:14
@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@BigSimmo

BigSimmo commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Organizing open PRs (Jul 6): Superseded in part by #274 on main. Rebase remaining unique hardening onto main if still needed.

@BigSimmo

BigSimmo commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded by merged work on \main:

Production is live with these fixes. Any remaining unique commits should be cherry-picked onto a fresh branch from current \main.

@BigSimmo BigSimmo closed this Jul 5, 2026
@BigSimmo
BigSimmo deleted the cursor/access-hardening-f048 branch July 9, 2026 13:01
cursor Bot pushed a commit that referenced this pull request Aug 8, 2026
Resolve docs/outstanding-issues.md by keeping main's #278 visual-baseline
row and renumbering this PR's follow-ups to #279#284 (next-id=285).

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
BigSimmo pushed a commit that referenced this pull request Aug 9, 2026
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo
BigSimmo pushed a commit that referenced this pull request Aug 9, 2026
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo
cursor Bot pushed a commit that referenced this pull request Aug 9, 2026
Keep main's shared #290-#293 queue. Drop duplicate pr-handoff #291 (already #284).
Archive this PR's #279 canvas-gate resolution alongside main's #264/#277 closures.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
BigSimmo added a commit that referenced this pull request Aug 9, 2026
…d reading mode, and the first canvas gate (#1772)

* test(viewer): gate the PDF canvas raster in a real browser

The viewer's raster surface had no browser proof: unit tests cover the raster
budget, DOM tests cover gestures, and a static contract covers the lazy
boundary, but none of them can see whether a clinical source page actually
paints. A blank canvas still reports correct dimensions, a correct aria-label,
and a resolved render promise.

tests/ui-document-canvas.spec.ts reads the raster back — ink pixels on page 1,
the real page count in the one toolbar readout, and a page flip whose FNV pixel
signature differs from page 1's. It also attaches an advisory page-flip cost
measurement (long tasks + time to paint) as the input the OffscreenCanvas
decision is conditioned on.

pdfjs-dist@6 calls Map.prototype.getOrInsertComputed, which ships in Chromium
151 but not in the 141 build some sandboxed containers pre-bake and pin via
PLAYWRIGHT_BROWSERS_PATH. The skip guard is therefore asymmetric: without CI it
skips with a reason naming the browser version; with CI set a missing engine
feature FAILS, because a gate that can skip itself green on the machine that
gates the merge is worse than no gate. Both directions were verified locally.

Spec collection is three hand-maintained lists that must agree, so all three are
updated together and tests/playwright-project-isolation.test.ts gains a
fail-closed assertion for this basename — "did not run" and "ran and skipped"
are indistinguishable in a log otherwise.

Closes #279 in docs/outstanding-issues.md. Its two refuted remedies (bump the
pinned Playwright build, pin pdfjs-dist down) were not actioned; the recorded
measurements were re-derived after install and match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* perf(viewer): virtualize the PDF reader into a windowed page column

The reader rasterised exactly one page into one canvas, so every page flip on a
long guideline was a cold pdf.js render. That is the remaining felt slowness in
the document view.

The viewer now renders a column of page slots and keeps a small window of them
rastered. Each slot reserves its page's box whether or not a canvas is currently
in it, so disposing a far page does not move the scroll position a reader
navigates by, and pages outside the window drop their backing store instead of
holding it until collection.

Three constraints shaped this and are resolved explicitly rather than deferred:

The raster budget is now document-wide. resolveCanvasRasterPlan bounds ONE
canvas against WebKit's ~2^24 ceiling and says nothing about how many exist, so
N individually-legal canvases could still exhaust device memory.
resolveLiveCanvasWindow caps total retained raster instead. Its useful property
is the curve, not the constant: a fit-width phone page never binds against it,
while a page at maximum zoom costs the whole per-canvas ceiling and collapses
the window to one — render-ahead disappears exactly where retaining neighbours
would be most dangerous, with no special-casing of zoom. MAX_CANVAS_PIXELS is
unchanged.

Render-ahead is reconciled with disableAutoFetch rather than trading it away.
Those flags exist because a reader looks at one page and pdf.js would otherwise
pull a whole guideline over cellular; rendering neighbours pulls exactly those
bytes back. Both flags stay, and the policy is bounded on three independent
axes: one page either side, deferred to requestIdleCallback so a fast flip never
pays for pages it passes, and switched off entirely under Save-Data or 2g. Three
resident pages, never the document.

Page sync stays one-way. Intent scrolls the column, scroll position derives the
displayed page, and a derived page writes the route only when it did not come
from a programmatic scroll. Two real races surfaced while testing this: the
route effect re-runs when pdf.js reports its page count, which is always after
the reader can have scrolled, so it now acts only when the route asks for
somewhere the reader is not; and the in-flight gate is armed when intent is
registered rather than a frame later when the scroll executes, since
intersections landing in that gap read as reader input and cancel the jump.

Multi-page documents get a bounded reading pane so the column is the thing that
scrolls; single-page documents keep their existing geometry exactly. The fit
scale now derives from the holder's content box rather than clientWidth minus a
fixed 16px, which was 16px short at sm:p-4 — invisible with one canvas, a layout
shift once slots reserve boxes from the same number.

Preserved: the per-run pageToCleanup isolation (Sentry 15801413), canvas zeroing
on dispose, the renderZoom debounce with its interim transform, and the
isLikelyExpiredUrl recovery path — which now also fires for a neighbour's range
403 while refusing to blank the reader's good page over a failed prefetch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* perf(viewer): window the document rail's figure cards

The rail mapped every clinical figure and every audit figure into a
DocumentImage on mount. That row is not cheap: it parses table markdown, decides
whether a structured AccessibleTable can render at all, computes quality
warnings and evidence tags, and mounts a SignedImage frame. A guideline with
ninety indexed tables paid all of it during hydration, before the reader had
opened the section, and the audit list underneath paid it again.

DocumentImageList renders a window of six and grows it as a sentinel comes into
view, with an explicit control to reveal the rest. Short lists — the
overwhelming majority of indexed documents — render whole and get no extra
chrome at all. The window is derived during render rather than synchronised in
an effect, so a list that shrinks underneath an expanded reader clamps
immediately instead of pointing past the end of the array for a frame.

The filmstrip is left whole on purpose: it is one button per figure with no
image behind it, and it is the cheap way to reach any page.

One correction to the Phase 3 brief, recorded in the test rather than assumed
either way. The brief says collapsed audit rows "still mint signed URLs".
SignedImage already defers its fetch behind an IntersectionObserver and a closed
<details> is display:none, so that claim is at least doubtful — but it is a
claim about real browser layout, and jsdom does no layout, so nothing available
here settles it. What is certain, and is what this commit removes, is the
mounting cost, which applies whether the section is open or shut. The rail's
observer also uses a 320px root margin rather than SignedImage's 640px, so the
two do not both run far ahead of the viewport once the section does open.

No virtualization dependency: rows have data-dependent heights, a windowed list
needs no measurement to be correct, and check:bundle-budget totals every built
chunk, so a library would land straight on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* perf(viewer): give deferred rail figures a lower fetch and decode priority

Two levers, both about the same thing: a secondary figure rail should not
contend with whatever the reader actually opened.

SignedImage now sets fetchPriority explicitly — high when the caller marked the
figure above-the-fold, low otherwise. next/image already emits decoding="async",
which governs when a decode blocks; fetch priority governs whether the image
competes for that budget at all, and it was the missing half of the pair.

The document rail passes a 240px IntersectionObserver root margin instead of the
shared 640px default. The wide default suits a surface whose images are the
point of the page; the rail's are not, and at 640px it minted signed URLs for
rows most of a viewport away, which land while the reader is looking at
something else. There is no cross-surface request scheduler, so this margin
differential is the ordering: surfaces on the wide default resolve first.

The 100-id batch signed-URL route stays unwired, deliberately. Beyond keeping a
privileged owner-scoped API route out of a component-only diff, the case for it
has actually weakened: windowing the rail to six rows means a figure-heavy
document no longer mounts N rows at once, which was the many-distinct-images
scenario the batch was meant to serve. Recorded on #283 with the measurement
that should decide it, rather than left as a standing assumption.

use-signed-image-url.ts is untouched. Its identity-in-the-dedupe-key and
cache-write-outside-the-shared-promise fixes were confirmed green before and
after (tests/auth-signed-url-cache.dom.test.tsx, 6 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* feat(viewer): complete the PDF reader's keyboard reading mode

The holder handled arrow keys, +/-, and 0. Phase 3 adds Page Up / Page Down,
Home / End, F for fit-to-width, and R for rotate.

Rotation needed a route back out. `rotation` arrives as a controlled prop with
no callback, so the keyboard could reach every viewing control except that one.
Rather than give the viewer its own rotation state — a second source of truth
for a single toolbar button — R calls the same `handlePdfRotate` that
DocumentFrame's rotate control already calls, threaded down as `onRotate`. When
no handler is supplied, R stays inert rather than swallowed: the event is not
preventDefault'ed, so it still reaches whatever else wants it.

Modified keystrokes are now explicitly ignored. Ctrl/Cmd+0 is the browser's own
zoom reset and Cmd+Left is history back on macOS; a reader that lost either to
the viewer would be worse off than one with no bindings at all.

The holder's aria-label names the bindings, so a screen-reader user hears them
on focus instead of having to discover them. Contract documented in
docs/wiring-conventions.md and covered by tests/document-viewer-keyboard.dom.test.tsx,
including the two rules that are easy to regress silently: only keystrokes aimed
at the holder itself are handled, and rotation goes through the frame's callback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* docs(viewer): record the Phase 3 outcome and the OffscreenCanvas decision

Phase 3's table said what to build; it now says what landed, including the two
items that deliberately did not.

Crop -> page overlay stays out: bbox is already SELECTed in document-detail.ts
but absent from DocumentDetailImage, so it is a contract change across
src/lib/**document** rather than a viewer change, and it is called out as the
one remaining Phase 3 capability with the shape of the work named.

OffscreenCanvas is not implemented, which is the plan's own instruction rather
than a shortcut — it conditions the work on "measured main-thread paint cost",
and no such measurement existed. Two things changed that. Virtualization keeps
the reader's page and a neighbour already rastered, so the cold-render-per-flip
cost that motivated a worker raster is largely gone before any threading work
starts; and the new canvas gate now attaches the number (flip-to-painted, long
task count and duration, backing pixels) on every Production UI run. #290
records how to read it and what result would close the question either way.
Nothing about this could be measured locally: pdfjs-dist@6 needs
Map.prototype.getOrInsertComputed, which this container's Chromium 141 lacks and
Node 24.13.0 lacks too, so no browser and no headless harness here can raster a
page at all.

Toolbar density is struck from the table — it shipped in Phase 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* test(viewer): keep the canvas gate out of the phone-chrome consumer list

The new spec imported blockExternalRequests from tests/helpers/phone-scroll,
which silently enrolled it in scripts/verify-phone-chrome.mjs's consumer list —
tests/verify-phone-chrome.test.ts asserts that list equals the set of specs
importing that helper, and went red. Enrolling it would have been wrong anyway:
this is a desktop raster gate and has nothing to do with phone chrome selection,
so it keeps a local copy of the request block instead, with a comment naming the
coupling so the next person does not re-import it.

Also records #291: tests/pr-handoff-stop.test.ts fails for any session running
as root, because it injects a write failure with chmod 0o555 and root ignores
directory write bits. Confirmed pre-existing on a clean origin/main worktree
with no local diff, so it is not from this branch — CI runs non-root and stays
green, and only container sessions ever see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* docs(issues): record the measured bundle-budget headroom on #252

A clean production build for this branch reports 306 client chunks at 1538.4 KiB
gzip against the 1406.4 KiB baseline captured 2026-08-04 — +9.4% inside a 10%
tolerance, so roughly 8 KiB of gzip headroom remains.

The drift is pre-existing rather than from this branch: Phase 3 adds no
dependency and its code delta is small. But it means the next feature-sized PR
of any kind trips check:bundle-budget whatever it touches, which turns #252's
open question — whether counting mockup chunks makes that a real signal — from
theoretical into the thing that decides how the next red build is read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* docs(ledger): record the Phase 3 review for PR #1772

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N496muQbJVbJW8XvkCgKc7

* fix(pdf-canvas-viewer): release canvas backing store on render=false and reset geometry on rotation

Co-authored-by: BigSimmo <87357024+BigSimmo@users.noreply.github.com>

* fix(viewer): key-repeat pageRef, live-canvas max budget, rail window reset

Relative keyboard moves read pageRef so key-repeat advances before React
re-renders. Budget retained canvases against the largest measured page cost.
Reset DocumentImageList on collectionKey. Soften #252 tip-only bundle wording;
tighten #294 OffscreenCanvas close criteria. Drop the PR-added legacy
shadow-tight on the empty page slot to keep the DS ratchet.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>

* fix(viewer): remount rail image list on document change via key

Avoid setState-in-effect for the expanded window reset; React remounts
DocumentImageList when the document-scoped key changes.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>

* style(tests): format document-rail image window test after prettier

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>

* docs(ledger): record PR 1772 review-and-fix at content tip 2cd72f1

Superseding heavy-scope row with decisive verify:cheap / verify:pr-local lines,
merge-tree clean, and the Phase 3 review dispositions.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant