14 file(s), +791 / -10 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +14 | -0 |
| messages/hi.json | Config | +14 | -0 |
| openapi/permission-map.json | Config | +3 | -9 |
| src/components/documents/register/living-index-strip.stories.tsx | Component | +98 | -0 |
| src/components/documents/register/living-index-strip.tsx | Component | +150 | -0 |
| src/components/matters/live-matter-documents-page.tsx | Component | +17 | -1 |
| src/lib/documents/register/adapter.test.ts | Library | +11 | -0 |
| src/lib/documents/register/adapter.ts | Library | +7 | -0 |
| src/lib/documents/register/bundle-index.test.ts | Library | +285 | -0 |
| src/lib/documents/register/bundle-index.ts | Library | +172 | -0 |
| src/lib/documents/register/filters.test.ts | Library | +1 | -0 |
| src/lib/documents/register/fixtures.ts | Library | +1 | -0 |
| src/lib/documents/register/index.ts | Library | +7 | -0 |
| src/lib/documents/register/types.ts | Library | +11 | -0 |
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.
src/lib/documents/register/bundle-index.ts — livingIndexes(),
a pure projection from the register corpus to one LivingIndex per carved bundle.src/components/documents/register/living-index-strip.tsx — the strip,
mounted above the register in live-matter-documents-page.tsx.DocumentsRegister.livingIndex in both messages/en.json
and messages/hi.json. The registrar's name comes from AgentsCast.kabir.name —
counsel reads Kabir; logs and dashboards keep saying Blake
(agents/_shared/personas.py: two namespaces, disagreeing by design).| What the strip shows | The committed fact behind it |
|---|---|
| The index exists at all | Carve: a row stamped family_role='main_document' with annexure children (materialize.py) |
| One row per member, in carve order | parentDocumentId + pageRange off those same rows |
A row ticks Read | That member carries Kabir's production card — context.document_cards, written inside the card drop's own transaction |
The index turns Final | A card on every member carve declared — BundleFamily.members + 1 (the head), frozen on the bundle row |
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.
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.
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).
DocumentListItem carries no family_manifest, so the broker's own
state/rows_read/generation and the filed index PDF's identity are not
readable here. The projection re-derives state from the member rows instead;
when functions exposes the manifest, reading it is a one-liner and the
"open the index" link becomes possible.indexed and the segment pass writes only documents.document_sections).
The strip therefore shows carve → per-member cards → final index, and says
nothing about segmentation rather than inventing a tick for it.clerk_worker/bundle/progress.py pushes run.progress to the run's
connection_id; an ambient Blake carve run has no browser connection, so every
push logs step_drop.progress_no_connection. Per the acceptance, live frames are
additive and never the source of truth — the durable half is the whole of the
truth, and it ships complete.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.