Segment-aware citation UX
A document does not cite the same way for every format. A PDF or Word file cites a page; a slide deck cites a slide; a spreadsheet cites a sheet + row. Quoting “page 7” of a PowerPoint is a lie a tribunal can catch. This story makes the Record cite honestly per segment type, stops the app from ever summing pages + slides + rows into one fake “pages” total, surfaces the new render states with their reasons, and marks the PRAMAAN-rendered copy we actually cite.
What changed
A new pure module src/lib/documents/segment-citation.ts
owns the citation grammar: SegmentKind =
page | slide | sheet_row, plus the i18n key each kind
resolves to. The CitationLabel primitive renders the
honest anchor — “page 12”, “slide 7”,
“Sheet ‘Q1 Ledger’, row 14” — never a
bare page number for a non-paged format.
Summary.totalPages (a single cross-document page sum)
is replaced by segmentCounts, a per-kind tally that the
engine accumulates into each document’s own kind. The
register footer, slicer, and group headers now render
“142 pages · 30 slides · 480 rows” side by
side — pages, slides, and rows are never collapsed into one
figure.
The detail drawer shows a PramaanRenderedBadge for
rendered Office docs, with a one-line note that citations bind to the
PRAMAAN-rendered PDF — while the original upload stays
downloadable through the existing privilege-gated download action.
The status pill now splits a status(reason) token and
shows a clear cause: unreadable(render_failed) →
“Unreadable · render failed”,
needs_review(render_partial),
needs_review(ocr_budget) — localized in
en + hi, never a raw enum token.
The citation grammar
| Format family | Segment kind | Citation form | Count form |
|---|---|---|---|
| PDF, DOCX, RTF, TXT | page |
page 12 | 24 pages |
| PPTX | slide |
slide 7 | 30 slides |
| XLSX, CSV | sheet_row |
Sheet ‘Q1 Ledger’, row 14 | 480 rows |
Contract status & seams (verified against merged main)
-
DR-8 segment model — NOT in the contract
yet. The OpenAPI mirror (
openapi/app-api.json) has nosegment_kind/segment_index/row_indexfields, sopnpm codegencannot produce them. Per the per-turn operating rule, the segment kind is derived from the document’s MIME type (defaultSegmentKind) and the citation index/row come from the real anchor once S1 ships. Every call site already speaksSegmentKind, so swapping the derivation for the wire field is a data change, not a refactor. -
DR-13
*_reason— FORWARD-READY.processingStatusis a free string on the wire with no separate reason field. The pill parses the documentedstatus(reason)notation off that string (parseProcessingStatus); when the backend adds a typed reason field, pass it directly and drop the parse. -
Inline PDF viewer — not a surface yet.
There is no in-app document viewer; the PRAMAAN-rendered badge lives in
the detail drawer where the document is viewed and downloaded. When a
viewer surface lands it reuses the same badge +
CitationLabel. -
No page added/moved. No
agentSurfacechange and nopnpm gen:routesrun were required.
Files touched
src/lib/documents/segment-citation.ts(+ test) — the citation grammar + per-kind tally.src/components/documents/citation-label.tsx(+ stories) — the typed anchor primitive.src/components/documents/segment-counts.tsx(+ stories) — per-kind summary, never summed.src/components/documents/detail/pramaan-rendered-badge.tsx(+ stories) — the rendered-copy badge.src/lib/documents/register/{types,adapter,view-state}.ts—segmentKindon the row;segmentCountsreplacestotalPages.src/components/documents/register/{presentation,register-cell,register-group-header,register-slice-navigator,document-status-pill}.tsx— per-kind rendering + reason parsing.src/components/documents/detail/{presentation.ts,document-detail-drawer.tsx}— rendered detection + drawer wiring.messages/{en,hi}.json—citation.*, statusreasons.*, detailrendered.*.
Verification
- Storybook — CitationLabel (all kinds), SegmentCounts (mixed/empty), PramaanRenderedBadge, and the new status-pill reason states each have a story.
pnpm test— 949 passing, incl. a segment-citation suite asserting kinds never commingle and a view-state test proving mixed kinds stay separate.pnpm lint/pnpm build/git diff --check— clean.- i18n parity — 0 keys missing across
en/hi.
Files changed
26 file(s), +600 / -77 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +23 | -1 |
| messages/hi.json | Config | +23 | -1 |
| src/components/documents/citation-label.stories.tsx | Component | +36 | -0 |
| src/components/documents/citation-label.tsx | Component | +29 | -0 |
| src/components/documents/detail/document-detail-drawer.tsx | Component | +15 | -2 |
| src/components/documents/detail/pramaan-rendered-badge.stories.tsx | Component | +15 | -0 |
| src/components/documents/detail/pramaan-rendered-badge.tsx | Component | +34 | -0 |
| src/components/documents/detail/presentation.ts | Component | +17 | -0 |
| src/components/documents/register/document-status-pill.stories.tsx | Component | +15 | -0 |
| src/components/documents/register/document-status-pill.tsx | Component | +19 | -7 |
| src/components/documents/register/presentation.ts | Component | +28 | -9 |
| src/components/documents/register/register-cell.tsx | Component | +4 | -1 |
| src/components/documents/register/register-group-header.stories.tsx | Component | +5 | -3 |
| src/components/documents/register/register-group-header.tsx | Component | +6 | -4 |
| src/components/documents/register/register-slice-navigator.tsx | Component | +16 | -17 |
| src/components/documents/segment-counts.stories.tsx | Component | +32 | -0 |
| src/components/documents/segment-counts.tsx | Component | +33 | -0 |
| src/lib/documents/register/adapter.test.ts | Library | +15 | -0 |
| src/lib/documents/register/adapter.ts | Library | +4 | -0 |
| src/lib/documents/register/fixtures.ts | Library | +1 | -0 |
| src/lib/documents/register/pagination.test.ts | Library | +3 | -3 |
| src/lib/documents/register/types.ts | Library | +19 | -6 |
| src/lib/documents/register/view-state.test.ts | Library | +23 | -8 |
| src/lib/documents/register/view-state.ts | Library | +27 | -15 |
| src/lib/documents/segment-citation.test.ts | Library | +64 | -0 |
| src/lib/documents/segment-citation.ts | Library | +94 | -0 |