PR: #568: feat(app): the section reading surface — outline, gutter marks, machine copy, JOURNEY [Story-2281] by @pramaan-board-bot[bot]
View PR →

Story-2281: the section reading surface (B4, EPIC-2270)

The backend now knows what the parts of a document ARE — cause title, grounds, prayer — and stores each as a typed range over (page, block) pairs (RFD-DOC-004, functions #647/#648/#649). This ticket is the half counsel can see: the outline that lists those parts, the gutter that says what is attached to each, the box that draws one across a page break, the machine copy that speaks the same coordinates, and the two acts counsel has over the machine's read.

The idea in one line

The section is the only anchor. A note keeps the span it always had (page + block + byte-exact quote, Story-2116); its enclosing section is derived by range containment at render — never a stored foreign key. That is what lets a re-segmentation move every mark without touching a single note, and what lets an OCR-scanned photocopy with no printed paragraph numbers anchor exactly like a born-digital pleading.

What was built

PieceWhereWhat it does
Section view-model viewer/sections-presentation.ts Flattens the tree into document order, derives the "Section 3 of 9" ordinal (top-level parts only), resolves a span to its deepest enclosing section, decides a section's open-edge treatment per page, and unions the blocks' bounding boxes into the section's box on that page. Pure; 12 unit tests.
Outline (D17) document-section-outline.tsx Collapsible right aside, ordinals, click-to-jump, one-line summary with no loading wait (the summary rides the same payload — SS-12). An unsegmented document says so honestly rather than showing an error.
Gutter marks + index (D22/D28) section-mark-popover.tsx One mark per KIND per section with a count — never a stack of glyphs (SS-22). The popover is the index. Keep/Dismiss appear only on the machine's own marks; a dismissal hides the mark but keeps it in an auditable dismissed index naming its dismisser, with an undo (SS-23).
Open-edge box (D4) section-open-edge-box.tsx The selected section only. First fragment closes top + sides, middles close sides, the last closes sides + bottom — no line is drawn at the break, so a section spanning pages reads as one unit (SS-02). The name rides above the opening edge, never over the page.
Machine copy (D11) document-machine-copy.tsx The extracted blocks rendered through react-markdown + remark-gfm (a recognised table reads as a table), cut into named sections, every block carrying data-page / data-block. Replaces the flat plain-text pane it supersedes.
JOURNEY + Re-read (D21/D30) document-journey.tsx, use-document-read-control.ts ✓ Received ✓ Scanned ✓ Read ✓ Registered, derived from the document's own status events — counsel never sees extracting or indexed. Beside it, the ONE standing action: "Re-read against the matter", which becomes the recovery door under an honest failure banner when the read did not finish.
Register bundles (D25) lib/documents/register/bundles.ts An opponent's paperbook folds into one collapsible family row carrying its annexure count, plus an Annexures filter on the shared SegmentedFilter. The filter runs before the view engine, so group summaries describe what counsel is looking at.

Two decisions worth naming

Live evidence (staging, 2026-07-12)

Verified end-to-end against real staging data (local app → staging gateway → staging Postgres), on document 2ff05fda-426c-4701-8de4-c8013844100a in matter M-015:

Two honest gaps, called out rather than papered over.
  1. No staging document has sections yet — segmentation has never run against the existing corpus, and the re-read ticket this surface queues is not being consumed on staging today. To prove the render path against the real API rather than fixtures, four sections were seeded directly into staging Postgres for the screenshots and then REMOVED (sections deleted, the coined kinds soft-deleted, the test note deleted via the API). Staging is back to zero sections.
  2. Scope item 4 (section boundary accept/reject, SS-10/SS-21) has no backend. The app API exposes only sections (GET), re-read and collation/undo — there is no endpoint to propose, split, merge or accept/reject a section boundary. The boundary edit that DOES exist upstream (the collation merge-back) is wired; the section-boundary proposal diff is not built, because building a UI over an API that does not exist is the empty-fixture failure this repo's doctrine forbids. It needs a functions ticket first.

Files changed

115 file(s), +7136 / -302 lines

Path Role + -
messages/en.jsonConfig+63-0
messages/hi.jsonConfig+63-0
openapi/app-api.jsonConfig+574-2
openapi/permission-map.jsonConfig+25-0
package.jsonConfig+2-0
pnpm-lock.yamlConfig+869-2
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/layout.tsxApp route+42-14
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/page.tsxApp route+4-1
src/app/[locale]/(workspace)/matters/[matterId]/record/doc/[documentId]/text/page.tsxApp route+12-12
src/app/api/internal/documents/[docId]/blocks/route.tsApp route+7-0
src/app/api/internal/matters/[matterId]/documents/[docId]/collation/undo/route.tsApp route+5-0
src/app/api/internal/matters/[matterId]/documents/[docId]/re-read/route.tsApp route+5-0
src/app/api/internal/matters/[matterId]/documents/[docId]/sections/route.tsApp route+5-0
src/components/documents/register/register-document-row.tsxComponent+35-1
src/components/documents/register/register-table.tsxComponent+31-4
src/components/documents/register/register-view.stories.tsxComponent+22-0
src/components/documents/register/register-view.tsxComponent+41-3
src/components/documents/viewer/annotations-presentation.tsComponent+28-1
src/components/documents/viewer/document-details-sheet.tsxComponent+4-0
src/components/documents/viewer/document-journey.stories.tsxComponent+59-0
src/components/documents/viewer/document-journey.tsxComponent+120-0
src/components/documents/viewer/document-machine-copy-view.tsxComponent+124-0
src/components/documents/viewer/document-machine-copy.stories.tsxComponent+66-0
src/components/documents/viewer/document-machine-copy.tsxComponent+177-0
src/components/documents/viewer/document-margin-note.tsxComponent+16-1
src/components/documents/viewer/document-original-pane.tsxComponent+118-17
src/components/documents/viewer/document-permalink-shell.stories.tsxComponent+23-6
src/components/documents/viewer/document-permalink-shell.tsxComponent+39-0
src/components/documents/viewer/document-reading-surface.tsxComponent+107-4
src/components/documents/viewer/document-section-outline.stories.tsxComponent+82-0
src/components/documents/viewer/document-section-outline.tsxComponent+120-0
src/components/documents/viewer/document-text-pane.stories.tsxComponent+0-28
src/components/documents/viewer/document-text-pane.tsxComponent+0-49
src/components/documents/viewer/document-text-view.tsxComponent+0-73
src/components/documents/viewer/journey-presentation.test.tsComponent+58-0
src/components/documents/viewer/journey-presentation.tsComponent+67-0
src/components/documents/viewer/live-presentation.test.tsComponent+1-23
src/components/documents/viewer/live-presentation.tsComponent+0-18
src/components/documents/viewer/machine-copy-presentation.tsComponent+80-0
src/components/documents/viewer/section-fixtures.tsComponent+198-0
src/components/documents/viewer/section-mark-popover.stories.tsxComponent+90-0
src/components/documents/viewer/section-mark-popover.tsxComponent+205-0
src/components/documents/viewer/section-open-edge-box.stories.tsxComponent+68-0
src/components/documents/viewer/section-open-edge-box.tsxComponent+58-0
src/components/documents/viewer/section-ordinal-chip.stories.tsxComponent+31-0
src/components/documents/viewer/section-ordinal-chip.tsxComponent+44-0
src/components/documents/viewer/sections-presentation.test.tsComponent+203-0
src/components/documents/viewer/sections-presentation.tsComponent+316-0
src/components/documents/viewer/use-document-annotations.tsComponent+63-13
src/components/documents/viewer/use-document-read-control.tsComponent+65-0
src/components/documents/viewer/use-document-sections.tsComponent+78-0
src/components/documents/viewer/use-section-labels.tsComponent+67-0
src/gen/client/appListMatterDocumentSections.tsGenerated contract+65-0
src/gen/client/appRereadMatterDocument.tsGenerated contract+67-0
src/gen/client/appUndoMatterDocumentCollation.tsGenerated contract+67-0
src/gen/client/index.tsGenerated contract+3-0
src/gen/hooks/index.tsGenerated contract+16-0
src/gen/hooks/useAppListMatterDocumentSections.tsGenerated contract+139-0
src/gen/hooks/useAppListMatterDocumentSectionsSuspense.tsGenerated contract+140-0
src/gen/hooks/useAppRereadMatterDocument.tsGenerated contract+157-0
src/gen/hooks/useAppUndoMatterDocumentCollation.tsGenerated contract+159-0
src/gen/index.tsGenerated contract+100-0
src/gen/schemas/authorityCited.jsonGenerated contract+5-0
src/gen/schemas/collationUndoResponse.jsonGenerated contract+32-0
src/gen/schemas/documentInsightBrief.jsonGenerated contract+41-1
src/gen/schemas/documentReReadResponse.jsonGenerated contract+32-0
src/gen/schemas/documentSectionNode.jsonGenerated contract+89-0
src/gen/schemas/documentSectionsResponse.jsonGenerated contract+26-0
src/gen/schemas/importCreateRequest.jsonGenerated contract+1-1
src/gen/schemas/importItemInput.jsonGenerated contract+1-1
src/gen/schemas/importItemInputKind.jsonGenerated contract+1-1
src/gen/schemas/insightPin.jsonGenerated contract+6-1
src/gen/schemas/positionAdmission.jsonGenerated contract+5-0
src/gen/schemas/punchCard.jsonGenerated contract+14-1
src/gen/schemas/silence.jsonGenerated contract+5-0
src/gen/types/AppListMatterDocumentSections.tsGenerated contract+74-0
src/gen/types/AppRereadMatterDocument.tsGenerated contract+81-0
src/gen/types/AppUndoMatterDocumentCollation.tsGenerated contract+81-0
src/gen/types/AuthorityCited.tsGenerated contract+1-1
src/gen/types/CollationUndoResponse.tsGenerated contract+33-0
src/gen/types/DocumentReReadResponse.tsGenerated contract+32-0
src/gen/types/DocumentSectionNode.tsGenerated contract+76-0
src/gen/types/DocumentSectionsResponse.tsGenerated contract+28-0
src/gen/types/ImportItemInputKind.tsGenerated contract+5-1
src/gen/types/InsightPin.tsGenerated contract+5-1
src/gen/types/PositionAdmission.tsGenerated contract+1-1
src/gen/types/PunchCard.tsGenerated contract+1-1
src/gen/types/Silence.tsGenerated contract+1-1
src/gen/types/index.tsGenerated contract+42-0
src/gen/zod/appListMatterDocumentSectionsSchema.tsGenerated contract+89-0
src/gen/zod/appRereadMatterDocumentSchema.tsGenerated contract+99-0
src/gen/zod/appUndoMatterDocumentCollationSchema.tsGenerated contract+99-0
src/gen/zod/authorityCitedSchema.tsGenerated contract+1-1
src/gen/zod/collationUndoResponseSchema.tsGenerated contract+29-0
src/gen/zod/documentReReadResponseSchema.tsGenerated contract+26-0
src/gen/zod/documentSectionNodeSchema.tsGenerated contract+60-0
src/gen/zod/documentSectionsResponseSchema.tsGenerated contract+37-0
src/gen/zod/importItemInputKindSchema.tsGenerated contract+1-1
src/gen/zod/index.tsGenerated contract+39-0
src/gen/zod/insightPinSchema.tsGenerated contract+9-2
src/gen/zod/positionAdmissionSchema.tsGenerated contract+1-1
src/gen/zod/punchCardSchema.tsGenerated contract+2-2
src/gen/zod/silenceSchema.tsGenerated contract+1-1
src/lib/documents/gateway.tsLibrary+114-0
src/lib/documents/matter-document-route-helper.tsLibrary+49-0
src/lib/documents/register/bundles.test.tsLibrary+55-0
src/lib/documents/register/bundles.tsLibrary+51-0
src/lib/documents/register/fixtures.tsLibrary+49-0
src/lib/documents/register/index.tsLibrary+6-0
src/lib/documents/register/pagination.test.tsLibrary+6-2
src/lib/documents/register/types.tsLibrary+7-0
src/lib/documents/register/view-state.tsLibrary+9-4
src/lib/kubb-client.tsLibrary+13-0
src/lib/query/invalidation.tsLibrary+68-0
src/lib/surface-registry/route-registry.tsLibrary+4-4

Verification