The forum is where a matter is actually heard — the Gujarat Electricity Regulatory Commission, the Bombay High Court, NCLT Mumbai. It sits at the top of the Brief's Case particulars rail, and until this change it was a wall: Vega read a tribunal name out of the documents and printed it. If the framer misread the venue, or the matter moved, counsel had no way to say so.
This is the first catalog-backed Editable Surface. Every promotion before it (Key instruments, Governing law, Relief) edited a frame fact — free text counsel owns outright. The forum is different: it is a pointer into a firm-managed catalog. That single difference is the whole Story, and it is what made the obvious implementation wrong.
The Brief's FORUM block renders two lines, and they are not the same kind of thing:
src/components/matter-detail/brief/matter-brief-sections.tsx const tribunal = brief.canonical.forum?.name || textField(frame, "tribunal_name"); // line 1 const forumType = textField(frame, "forum_type") || matter.forumType || ""; // line 2
EditableChoice that writes the frame's tribunal_name. That is a
silent swallow. The rail reads canonical.forum?.name
first — so for exactly the matters that HAVE a forum, the frame edit is persisted and
never displayed. The optimistic value paints, the invalidate lands, and the old name snaps
back. This is the same trap already scarred into this very file for value-at-stake
(matter-brief-page.tsx), which is why that field writes through the canonical
matter record instead.
The real home of the forum is not the frame at all:
MatterBriefCanonicalView.forum
→ "The Main proceeding's forum (proceedings.forum_id -> forums);
null when the Main proceeding has no forum set."
So the forum lives on proceedings.forum_id, pointing into the firm's
forum catalog — the same catalog Settings already manages. To change it you
must PATCH the proceeding, not the frame.
The muted second line (forum_type, e.g. "Domestic arbitration") looks like it
wants a picker too. It does not get one here, because no firm catalog enumerates its
vocabulary:
| Catalog | What is actually in it | Fits "Domestic arbitration"? |
|---|---|---|
forums | Supreme Court of India, NCLT Mumbai, Sessions Court Delhi | No — these are venues (line 1) |
proceeding-types | Written submissions, Rejoinder, Affidavit of evidence | No — these are filing kinds |
matter-types | Domestic Arbitration, Writ Petition, Civil Suit | Yes — it is the matter type |
forum_type frame fact — which feeds agent context and Postgres FTS. That
is contract corruption, not a UI nit. The contract agrees the field is confused:
MatterDetailView.forumType is documented as "Deprecated display compatibility
value. Now derived from the firm matter type joined via matter_type_id."
docs/design/review-editable-matter-frame.md §6 already flags this exact conflation
as an open data-model cleanup. Line 2 stays read-only and is promoted under that
ruling, not this Story.
useAppUpdateMatterProceeding
mutation had no browser→server route, so it would have 404'd. Per AGENTS.md ("build the proxy
as step one"): a new PATCH handler at
/api/internal/matters/[matterId]/proceedings/[proceedingId], its
toBrowserRoute mapping, and a mattersGateway.updateProceeding
method. Inbound JSON is parsed with the generated request schema and the response is
re-asserted against ProceedingView (HR-21) — no as T at the
boundary.
ForumChoice loads the firm's catalog via
useAppListForums and writes the Main proceeding's forumId. The Main
proceeding is found on the detail payload the page already holds
(matter.proceedings.find(p => p.isMain)) — no extra fetch, and
no backend change was needed.
EditableChoice grows a
search box (ux-guidelines §1: a long blind list is the same trap as a blind text input). The
caller never decides — there is no searchable prop to get wrong. Radix's menu
typeahead would otherwise eat the keystrokes, so the search input stops propagation for
everything except Arrow/Enter/Escape/Tab, which the menu still owns.
canonical.forum is patched immediately; the PATCH runs behind it; a rejection
restores the snapshot and toasts. The PATCH carries only the id, so the picked name
(which is what the rail renders) rides along in a ref.
EditableChoice gained an
emptyContent slot, and the forum picker fills it with a link into the Settings
forum catalog. This is the first-run state a mock-fixture-only pass never sees — AGENTS.md:
"done" = system boundary, not component boundary; fixtures lie in your favour.
It has its own Storybook story (EmptyCatalog), and the Brief page's own fixture was
corrected in the same change: its proceedings array had no isMain
flag, which would have silently rendered the picker read-only — the fixture lying, not the UI
working.
12 file(s), +615 / -35 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +3 | -0 |
| messages/hi.json | Config | +3 | -0 |
| src/app/api/internal/matters/[matterId]/proceedings/[proceedingId]/route.ts | App route | +64 | -0 |
| src/components/matter-detail/brief/forum-choice.stories.tsx | Component | +200 | -0 |
| src/components/matter-detail/brief/forum-choice.tsx | Component | +81 | -0 |
| src/components/matter-detail/brief/matter-brief-page.stories.tsx | Component | +35 | -2 |
| src/components/matter-detail/brief/matter-brief-page.tsx | Component | +55 | -1 |
| src/components/matter-detail/brief/matter-brief-sections.tsx | Component | +29 | -29 |
| src/components/shared/editable-choice.stories.tsx | Component | +42 | -0 |
| src/components/shared/editable-choice.tsx | Component | +71 | -3 |
| src/lib/kubb-client.ts | Library | +13 | -0 |
| src/lib/matters/gateway.ts | Library | +19 | -0 |
| Story | What it proves |
|---|---|
MatterDetail/ForumChoice → Populated | 9 forums → the picker is search-first; a selected forum shows its name. |
MatterDetail/ForumChoice → EmptyCatalog | First run: no blank menu — a route out to Settings. |
MatterDetail/ForumChoice → AdHocTribunal | No catalog row for "a retired judge sitting alone": Vega's extracted tribunal text survives as the placeholder, catalog still open. |
MatterDetail/ForumChoice → ReadOnly | Shown, never editable. |
MatterDetail/ForumChoice → RollbackOnError | Pick applies instantly, server rejects, value snaps back. |
Shared/EditableChoice → Searchable / EmptyCatalog | The two new capabilities on the primitive itself. |
pnpm codegen; no OpenAPI mirror change; no pramaan-functions change. Both endpoints already existed.forum_type line is unchanged (see above).RailFact — whose only caller was the old read-only forum block — was removed rather than left orphaned.