Counsel marks a document “Privileged” on the Record
Every document a firm uploads enters a matter’s Record — searchable, producible, usable in filings. Some of it must never go out: attorney work product, litigation strategy. Producing it by mistake is waiver — the worst-case legal failure. This story ships the manual, lawyer-driven control to withhold a document, plus the shared legal vocabulary every future surface will reuse. The backend gate already exists (pramaan-functions Story-1501, FN-PRIVILEGE); this is the app-side UI + wiring. Founder stance (locked): a clear warning and counsel holds the control — no AI auto-decision, no heavy hand-holding.
What changed
- Legal-lexicon module —
src/lib/legal-lexicon/: typed concept keys → jurisdiction-aware labels (concepts.ts), a purelegalTerm(key, { jurisdiction })resolver (resolve.ts, defaultUS), and theuseLegalTerm(key)hook (use-legal-term.ts). A mirror of the canonical lexicon. Every legal word (“Privileged”, “Work product”, “Record”) now renders ONLY from here — never an inline string, never a next-intl locale key. V1 wires the US column; other jurisdictions are a data addition, not a refactor. - Counsel-only privilege control —
src/components/documents/detail/privilege-control.tsx, shown under the timeline in the document drawer. Visible only to firm_admin / firm_editor (matters:write); a read-only member or support session sees nothing. Marking and clearing each require an explicit confirm stating the document leaves / returns to the Record. - Wiring to the existing endpoint — the drawer container
calls the generated
useAppSetMatterDocumentPrivilegemutation; a new BFF proxy (/api/internal/matters/[matterId]/documents/[docId]/privilege) +toBrowserRoutemapping keep the browser on/api/internal/*. The proxy and the server gateway (documentsGateway.setPrivilege) parse the request and response against the generated Zod contract at the trust boundary (HR-21). No contract was hand-written — the endpoint already shipped in the OpenAPI mirror, so codegen was untouched. - List reflects the flip — on success the updated row is
bubbled up to the register state (instant Privileged badge) and
router.refresh()re-runs the RSC record bootstrap to reconcile with the server, which has already gated the doc out of the evidence search index and Mode-2 processing. - Copy discipline — non-legal scaffolding is next-intl in
both
messages/en.jsonandmessages/hi.jsonunderDocumentsRegister.detail.privilege; the legal labels are interpolated in from the lexicon.
The two axes (why Privileged ≠ Confidential)
| Concept key | US label | Meaning |
|---|---|---|
record | Record | the trustworthy home of a matter’s documents |
protection.producible | Producible | can be filed, produced, relied on (the default) |
protection.privileged | Privileged | withheld entirely — cannot be compelled |
protection.privileged.workProduct | Work product | strategy / mental impressions prepared for litigation |
Privilege is Axis-1 (can it go out?). The W1 upload buckets are Axis-2 (whose is it?). The axes never merge. This story ships the Axis-1 keys the control needs; the rest of the lexicon is recorded for later waves.
Verification
- Storybook-first:
PrivilegeControlstories cover idle · confirm · privileged · updating · read-only-hidden; the drawer stories add the integrated counsel view. Each verified in isolation, no backend. pnpm test— 933 green, incl. thelegalTermresolver test and the mark/clear confirm-flow test (confirm gates the call; read-only renders nothing).pnpm lintclean;pnpm buildgreen with the new privilege proxy route registered.- No color literals; no new dependencies; no hand-written contract.
Out of scope (named later, per the lexicon): Blake auto-suggestion, the Work Product workspace, redaction, and confidential / AEO.
Files changed
20 file(s), +824 / -1 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +24 | -0 |
| messages/hi.json | Config | +24 | -0 |
| src/app/[locale]/(workspace)/matters/[matterId]/record/page.tsx | App route | +1 | -0 |
| src/app/api/internal/matters/[matterId]/documents/[docId]/privilege/route.ts | App route | +83 | -0 |
| src/components/documents/detail/document-detail-drawer-container.tsx | Component | +88 | -0 |
| src/components/documents/detail/document-detail-drawer.stories.tsx | Component | +18 | -0 |
| src/components/documents/detail/document-detail-drawer.tsx | Component | +24 | -0 |
| src/components/documents/detail/privilege-control.stories.tsx | Component | +71 | -0 |
| src/components/documents/detail/privilege-control.test.tsx | Component | +79 | -0 |
| src/components/documents/detail/privilege-control.tsx | Component | +133 | -0 |
| src/components/documents/register/saved-views-register.tsx | Component | +13 | -0 |
| src/components/matters/live-matter-documents-page.tsx | Component | +19 | -0 |
| src/lib/documents/gateway.ts | Library | +30 | -1 |
| src/lib/kubb-client.ts | Library | +12 | -0 |
| src/lib/legal-lexicon/concepts.ts | Library | +77 | -0 |
| src/lib/legal-lexicon/index.ts | Library | +14 | -0 |
| src/lib/legal-lexicon/resolve.test.ts | Library | +33 | -0 |
| src/lib/legal-lexicon/resolve.ts | Library | +46 | -0 |
| src/lib/legal-lexicon/use-legal-term.ts | Library | +26 | -0 |
| src/lib/pramaan-diagnostics.ts | Library | +9 | -0 |