PR: #570: feat(app): the document permalink v2 — slim strip, two gutters, Insights dossier [Story-2294] by @pramaan-board-bot[bot]
View PR →

Story-2294: the document permalink v2 (B5b, EPIC-2270)

The section surface (app#568) and the CCB chassis (app#569) shipped the substrate. This ticket gives it the shape counsel actually reads in: one slim strip, the document flanked by two gutters, and a dossier where the machine keeps everything it knows.

The idea in one line

Two voices, two gutters, one dossier. The machine speaks in the LEFT margin, the team speaks in the RIGHT one, and neither ever writes on the paper. Everything the machine knows about the document — its read, its findings, its outline, its identity, its history — lives in one place: the Insights lens.

What was three bands is now one line

The permalink used to spend a doc ribbon (64px), a Download/Permalink/Details sub-ribbon (56px) and a viewer toolbar (44px) before the document began — plus a 340px "What matters here" sidebar taking the width. All of it is gone.

WasIs
Doc ribbon + sub-ribbon + toolbar (~164px)ONE 46px strip: name · state · pages · lens tabs · one clustered control · ⋮
"Details" button → right-edge sheetNo button. Details is a block in the Insights dossier.
340px key-facts sidebarGone. "What matters here" is a dossier block.
Section outline aside beside the pageGone. The outline is a dossier block; a row lands on Original AT that section.
Gutter popover over the pageBanned. The finding cards expand IN the left gutter.
Floating counsel bar on the paragraphGone. The selection publishes to the CCB, which already lives in the shell.
"Machine copy" / "PROCESSED""Text" / the ADR-128 words: PROCESSING · INDEXED · REGISTERED.

The two voices (the load-bearing rule)

An annotation the machine proposed is a finding: it is the machine asking counsel to rule on something, so it rides the LEFT gutter with the curation the substrate can actually record (Keep / Dismiss, optimistic). The moment counsel keeps it, it stops being a finding and becomes a note — AGENT-badged, in the RIGHT gutter, with the team's. A brief punch card is an insight: real, span-pinned, but with no dismissal contract upstream, so its card offers only its door into Insights, never a button that would lie about being recorded.

proposed agent annotation  -> LEFT  (contradiction, curatable)
brief punch card           -> LEFT  (insight, read-only)
counsel note               -> RIGHT (conversation)
kept agent note            -> RIGHT (conversation, AGENT badge)
dismissed                  -> the auditable dismissed index (never deleted)

This is buildSectionGutters in sections-presentation.ts, and it is why the CCB's contradiction verb — which had no producer until now — finally fires.

Where the pieces live

ConcernFile
The two-voice split, by range containment (D10)viewer/sections-presentation.ts (buildSectionGutters, sectionSignals, firstBlockOfSection)
The slim strip + its ADR-128 state vocabularyviewer/document-strip.tsx, viewer/strip-presentation.ts
Mirrored rails on the pageviewer/section-rails.tsx
Left gutter: pills → expand-in-gutter cardsviewer/section-machine-gutter.tsx, viewer/section-finding-card.tsx, viewer/section-gutter-marks.tsx
Right gutter: note stack, collapse group, composerviewer/section-note-stack.tsx, viewer/document-note-composer.tsx
The Original lens (page + rails + both gutters)viewer/document-original-lens.tsx, viewer/document-original-pane.tsx
The Insights dossierviewer/document-insights-dossier.tsx + viewer/insights/dossier-*.tsx
Focus mode + page/zoom seamviewer/reading-mode-context.tsx, viewer/focus-exit-chip.tsx, viewer/document-permalink-shell.tsx
The dossier's Audit block (new seam)lib/audit/gateway.ts → api/internal/audit/matters/[matterId]/documents/[documentId]/route.ts → useAppDocumentAuditTimeline

Focus mode without touching the shell

Ruling 1 says the matter shell (ribbon, work nav, rails) is untouched, and only focus mode may hide it at runtime. So focus mode edits nothing: the viewer takes the frame (fixed inset-0) while it is on, retracts both gutters, and gives the frame back on Esc. The shell is still mounted and none of its components know this happened. The CCB stays — reading is not silence — and a slim hover edge at the top brings the strip back without leaving focus, so counsel can turn a page and keep reading.

Anchors: a note's quote is always the record's

"+ Add a note" can be pressed without dragging a passage. The composer then anchors to the section's first block — its real extracted text, from the same run the note will be byte-verified against (firstBlockOfSection). When no block resolves, the affordance is withheld. A synthesized quote would be a span the record cannot verify, and provenance-is-law does not bend for a UI convenience.

Data honesty — "What matters here". There is no label/value key-facts read contract for a document: document_card.v1 rides an event into the matter's context bundle, and no endpoint serves it back. So the block renders the brief's punch cards (lead · consequence · span pin) — the machine's real, pinned points — rather than a fabricated fact table. A fake row wearing a real pin would be the worst of both. When a card read API lands, this block swaps to it; the frame's label/value shape is the target.
Verification. pnpm lint (0 errors), pnpm test (1130 passed, incl. new buildSectionGutters / toStripStatus / firstBlockOfSection suites), pnpm build, pnpm check:routes (38 surfaces fresh), pnpm storybook:check. Every new component ships a co-located story, and the animated ones (gutter expand/collapse, note-group open/fold, focus chip) ship an interactive story that demonstrates enter AND exit inside AnimatePresence.

Files changed

81 file(s), +3927 / -3537 lines

Path Role + -
messages/en.jsonConfig+43-53
messages/hi.jsonConfig+45-55
openapi/app-api.jsonConfig+1-1
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/insights/page.tsxApp route+47-13
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/layout.tsxApp route+28-55
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/page.tsxApp route+14-9
src/app/api/internal/audit/matters/[matterId]/documents/[documentId]/route.tsApp route+46-0
src/components/documents/ccb/ccb-presentation.tsComponent+4-2
src/components/documents/viewer/agent-note-card.tsxComponent+0-46
src/components/documents/viewer/citation-landing-pill.tsxComponent+0-44
src/components/documents/viewer/document-badges.tsxComponent+1-66
src/components/documents/viewer/document-control-bar.stories.tsxComponent+0-48
src/components/documents/viewer/document-counsel-bar.stories.tsxComponent+0-36
src/components/documents/viewer/document-counsel-bar.tsxComponent+0-82
src/components/documents/viewer/document-details-sheet.stories.tsxComponent+0-51
src/components/documents/viewer/document-details-sheet.tsxComponent+0-165
src/components/documents/viewer/document-fact-rail.stories.tsxComponent+0-77
src/components/documents/viewer/document-fact-rail.tsxComponent+0-166
src/components/documents/viewer/document-fact-row.tsxComponent+0-88
src/components/documents/viewer/document-gutter-mark.tsxComponent+0-60
src/components/documents/viewer/document-insights-dossier.stories.tsxComponent+171-0
src/components/documents/viewer/document-insights-dossier.tsxComponent+100-0
src/components/documents/viewer/document-insights-live.tsxComponent+152-9
src/components/documents/viewer/document-insights-view.stories.tsxComponent+0-36
src/components/documents/viewer/document-insights-view.tsxComponent+0-174
src/components/documents/viewer/document-machine-copy-view.tsxComponent+1-6
src/components/documents/viewer/document-margin-note.tsxComponent+0-48
src/components/documents/viewer/document-note-item.tsxComponent+0-46
src/components/documents/viewer/document-notes.stories.tsxComponent+0-46
src/components/documents/viewer/document-original-lens.tsxComponent+265-0
src/components/documents/viewer/document-original-pane.tsxComponent+101-303
src/components/documents/viewer/document-permalink-shell.stories.tsxComponent+0-134
src/components/documents/viewer/document-permalink-shell.tsxComponent+152-218
src/components/documents/viewer/document-processing-stages.tsxComponent+0-63
src/components/documents/viewer/document-reading-surface.tsxComponent+0-309
src/components/documents/viewer/document-strip.stories.tsxComponent+110-0
src/components/documents/viewer/document-strip.tsxComponent+279-0
src/components/documents/viewer/document-viewer-header.tsxComponent+0-104
src/components/documents/viewer/focus-exit-chip.stories.tsxComponent+54-0
src/components/documents/viewer/focus-exit-chip.tsxComponent+40-0
src/components/documents/viewer/insights/dossier-assertions.tsxComponent+92-0
src/components/documents/viewer/insights/dossier-audit.tsxComponent+69-0
src/components/documents/viewer/insights/dossier-block.tsxComponent+64-0
src/components/documents/viewer/insights/dossier-contradictions.tsxComponent+85-0
src/components/documents/viewer/insights/dossier-details.tsxComponent+115-0
src/components/documents/viewer/insights/dossier-notes.tsxComponent+68-0
src/components/documents/viewer/insights/dossier-outline.tsxComponent+65-0
src/components/documents/viewer/insights/dossier-read.tsxComponent+48-0
src/components/documents/viewer/insights/dossier-what-matters.tsxComponent+55-0
src/components/documents/viewer/live-presentation.tsComponent+7-54
src/components/documents/viewer/reading-mode-context.tsxComponent+46-0
src/components/documents/viewer/section-finding-card.stories.tsxComponent+48-0
src/components/documents/viewer/section-finding-card.tsxComponent+101-0
src/components/documents/viewer/section-fixtures.tsComponent+45-42
src/components/documents/viewer/section-gutter-marks.stories.tsxComponent+45-0
src/components/documents/viewer/section-gutter-marks.tsxComponent+86-0
src/components/documents/viewer/section-machine-gutter.stories.tsxComponent+63-0
src/components/documents/viewer/section-machine-gutter.tsxComponent+100-0
src/components/documents/viewer/section-mark-popover.stories.tsxComponent+0-90
src/components/documents/viewer/section-mark-popover.tsxComponent+0-228
src/components/documents/viewer/section-note-stack.stories.tsxComponent+75-0
src/components/documents/viewer/section-note-stack.tsxComponent+219-0
src/components/documents/viewer/section-rails.stories.tsxComponent+75-0
src/components/documents/viewer/section-rails.tsxComponent+61-0
src/components/documents/viewer/sections-presentation.test.tsComponent+92-47
src/components/documents/viewer/sections-presentation.tsComponent+107-78
src/components/documents/viewer/strip-presentation.test.tsComponent+46-0
src/components/documents/viewer/strip-presentation.tsComponent+75-0
src/components/documents/viewer/types.tsComponent+13-70
src/components/documents/viewer/use-document-gutters.tsComponent+148-0
src/components/documents/viewer/use-document-sections.tsComponent+10-41
src/components/documents/viewer/use-section-labels.tsComponent+31-17
src/components/documents/viewer/viewer-fixtures.tsxComponent+0-183
src/gen/schemas/bundleSection.jsonGenerated contract+1-1
src/gen/schemas/contextBundle.jsonGenerated contract+1-1
src/gen/schemas/documentCard.jsonGenerated contract+1-1
src/gen/types/DocumentCard.tsGenerated contract+1-0
src/gen/zod/documentCardSchema.tsGenerated contract+74-67
src/lib/audit/gateway.tsLibrary+26-0
src/lib/kubb-client.tsLibrary+11-0
src/lib/surface-registry/route-registry.tsLibrary+4-4