PR: #788: feat(record): the living index on the matter screen [Story-3301] by @pramaan-board-bot[bot]
View PR →

Files changed

14 file(s), +791 / -10 lines

Path Role + -
messages/en.jsonConfig+14-0
messages/hi.jsonConfig+14-0
openapi/permission-map.jsonConfig+3-9
src/components/documents/register/living-index-strip.stories.tsxComponent+98-0
src/components/documents/register/living-index-strip.tsxComponent+150-0
src/components/matters/live-matter-documents-page.tsxComponent+17-1
src/lib/documents/register/adapter.test.tsLibrary+11-0
src/lib/documents/register/adapter.tsLibrary+7-0
src/lib/documents/register/bundle-index.test.tsLibrary+285-0
src/lib/documents/register/bundle-index.tsLibrary+172-0
src/lib/documents/register/filters.test.tsLibrary+1-0
src/lib/documents/register/fixtures.tsLibrary+1-0
src/lib/documents/register/index.tsLibrary+7-0
src/lib/documents/register/types.tsLibrary+11-0

Story-3301 — the P5 progress UI: the living index on the matter screen

pramaan-functions Story-3300 files the living index: a draft the moment the knife commits a bundle's members, one row upgraded per card, a final PDF into the family. This is the app half — counsel watching that index fill in on the Record, with Kabir's name on it.

What ships

Where each tick comes from — real work, never a timer

What the strip showsThe committed fact behind it
The index exists at allCarve: a row stamped family_role='main_document' with annexure children (materialize.py)
One row per member, in carve orderparentDocumentId + pageRange off those same rows
A row ticks ReadThat member carries Kabir's production card — context.document_cards, written inside the card drop's own transaction
The index turns FinalA card on every member carve declared — BundleFamily.members + 1 (the head), frozen on the bundle row

Completeness comes from the declared family, never from one page of rows

The Record renders one page of documents (limit=50 on the record bootstrap). A paperbook carved into more members than that has its later annexures absent from the corpus entirely — so a final decided from rows.length would call the index complete while the backend family is still draft. Telling counsel a legal index is finished while annexures are pending is the exact class of credible-looking-but-wrong status this epic exists to kill (app#788 P1).

So the denominator is familyMemberCount — carve's declared member set, read off the frozen BundleFamily already on the bundle row, durable and in the payload the page already has (no extra fetch, no poll). The strip shows that real denominator ("12 of 62 read") and names how many members are off the page. A family whose declared count is missing stays draft forever rather than guessing. bundle-index.test.ts → completeness comes from the DECLARED family reds if the predicate goes back to rows.length.

Refresh is the test (D22)

The strip reads the same server-rendered corpus the register renders — the Record page's existing recordBootstrap round-trip. No new endpoint, no poll, no second fetch. Reload the browser mid-journey and the projection re-derives an identical index; bundle-index.test.ts asserts exactly that, including corpus-order independence.

It cannot run ahead of the work. Three ways it errs, all in the same safe direction: a card that applied REFUSED (the broker's unread, which the app contract does not carry) reads pending; members off the current page keep the index draft; and a missing declared count keeps it draft forever. It can under-report a finished bundle; it cannot over-report an unfinished one.

Firm scoping

The corpus is one firm-scoped read (backend RLS + the Clerk org scope on the query client). The projection groups members by a parentDocumentId that resolves inside that corpus, fetches nothing, and resolves no id of its own — so a member whose parent is not visible produces no index and never stitches itself into a family that is. Asserted in bundle-index.test.ts → firm scoping (RLS).

Not derivable from today's app contract — named, not faked

Verify

Storybook: Documents/Register/LivingIndexStrip — DraftAtCarve, FillingIn, Final, LargerThanOnePage, TwoBundles, Empty.
Staging: open a matter holding a carved bundle → the strip lists its members with Kabir reading them; reload mid-journey → identical progress; when the last card lands the strip flips to Final.