Story-2435 · pramaan-app · 15 Jul 2026
Every citation gets one door back to the exact page.
The document URL now accepts a page: .../doc/<id>?page=14
opens the Original scrolled to page 14. This is the first linkage primitive of
the Insight Layer (Epic-2423) — without it, “the machine speaks from
the margins” has no way to point counsel at the source it cites. The scroll
itself already worked; what Story-2435 adds is the one shared way
every surface builds and reads that link, so the grammar can never drift.
The one idea
?page=), never a route segment (route-per-mode doctrine). One helper
writes the link, one helper reads it back, and both live in a single module —
so the insight, chronology, contradiction and Brief surfaces that will cite pages
tomorrow all speak the same grammar as the Insights dossier does today.
What shipped
| Piece | What it does |
|---|---|
| Shared page-permalink module |
New src/lib/documents/page-permalink.ts owns both directions:
buildDocumentPageHref(base, page, { section }) appends
?page=N (and optional §ion=) via
URLSearchParams; readPagePermalink(searchParams) parses
it back. The page/section query keys are constants
exported here, defined once.
|
| Five dossier links route through it |
dossier-assertions, dossier-what-matters,
dossier-contradictions, dossier-outline and
dossier-notes dropped their hand-built
`${originalHref}?page=...` template literals for the helper. No
call site string-builds a page URL any more.
|
| The pane reads through it |
document-original-pane swapped its inline
Number.parseInt(searchParams.get("page")) for
readPagePermalink, and reads section through the shared
SECTION_PARAM constant. On mount it pushes the landing page UP to
the shell as a request, since the page lives in the strip.
|
How it degrades (clamp/ignore, no crash)
| URL | Result | Where |
|---|---|---|
?page=14 |
scrolls to page 14 | read → request → scrollToIndex(13) |
?page=99999 (count 40) |
scrolls to the last page | Math.min(page, pageCount) in the scroll column |
?page=0 / ?page=-3 |
opens at the top (which IS page 1) | readPagePermalink returns null |
?page=garbage / absent |
opens at the top, no request fired | readPagePermalink returns null |
Why the upper clamp lives downstream
The reader cannot clamp > pageCount — on mount, a landing page
is asked for before pdf.js has reported how many pages the document even
has. So the reader only floors at 1, and the scroll column applies
Math.min(pageRequest.page, pageCount) - 1 once the count is known
(its effect depends on pageCount for exactly this reason). One clamp,
in the only place that knows the count.
The read path
Coexists with what already shipped
The landing page is a PageRequest pushed to the shell — the same
channel the strip's page box, the keyboard nav (Story-2318) and the section pins
already use. It touches neither the per-document zoom store (Bug-2356 / Story-2367)
nor F-focus mode (Story-2348): those are untouched state, and a page request rides
straight past them.
No functions change
Confirmed: page count and scroll are entirely client-side. The founder flagged a
possible endpoint; analysis holds there is none needed — no
pramaan-functions change, and no separate functions ticket filed.
Optional debounced write-back — left out
Updating ?page= on scroll (so copying the URL mid-read preserves
position) was scoped as optional-if-trivial. It is not trivial here: the page moves
on every scroll frame, so it needs a debounce plus a router.replace
that must not re-trigger the read effect or push history entries. Left out per the
ticket; the deep-link (read) direction is complete.
Verify
pnpm lint— 0 errors (pre-existing warnings only).pnpm test— newpage-permalink.test.ts(7) green; viewer suite (67) green.pnpm build— green (typecheck clean).-
Staging UAT (matter 15):
/en/matters/15/record/doc/4288279d-65b2-4ffd-ac46-530ec06150b5?page=3opens the Original scrolled to page 3.
Files changed
8 file(s), +128 / -9 lines
| Path | Role | + | - |
|---|---|---|---|
| src/components/documents/viewer/document-original-pane.tsx | Component | +8 | -4 |
| src/components/documents/viewer/insights/dossier-assertions.tsx | Component | +2 | -1 |
| src/components/documents/viewer/insights/dossier-contradictions.tsx | Component | +3 | -1 |
| src/components/documents/viewer/insights/dossier-notes.tsx | Component | +4 | -1 |
| src/components/documents/viewer/insights/dossier-outline.tsx | Component | +4 | -1 |
| src/components/documents/viewer/insights/dossier-what-matters.tsx | Component | +3 | -1 |
| src/lib/documents/page-permalink.test.ts | Library | +49 | -0 |
| src/lib/documents/page-permalink.ts | Library | +55 | -0 |