Governing law is the list of acts and statutes a matter actually runs on — the
Arbitration and Conciliation Act, a state stamp act, a contract's chosen law. It sits in the
Brief's Case particulars rail, and until this change it was a wall: Vega read
it out of the documents, joined it with ·, and printed it. If the framer misread a
statute, or missed one, counsel had no way to say so. The most load-bearing legal fact on the
page was the one nobody could touch.
This Story is pure reuse. Story-2355 built EditableList and put
Key instruments on it; Story-2360 put three more lists on it. Governing law is the fifth
consumer. No new primitive, no new editor, no new data path, no pnpm codegen, no
backend change.
A field is only worth making editable if the write path will actually accept it. The frame edit
endpoint (PATCH …/frame) declares fields as a free-form object in
OpenAPI — additionalProperties: true — so the OpenAPI mirror alone cannot answer
the question. The real allowlist is enforced server-side, in pramaan-functions:
src/domains/matter_frame/frame_contract.py
_META_FIELDS = frozenset({"frame_status", "missing_core_fields"})
EDITABLE_FRAME_FIELDS = frozenset(FrameMirror.model_fields) - _META_FIELDS
Every FrameOutput field except Vega's two honesty-gate meta fields is editable.
governing_law is neither of those, and it carries exactly the shape
key_instruments already proved:
| Where | What it says |
|---|---|
src/domains/matter_frame/dtos.py:660 | governing_law: list[str] = Field(default_factory=list) |
schemas/frame_output.v1.schema.json | "governing_law": { "items": { "type": "string" }, … } — array<string> |
schemas/frame_output.v1.fields.json | listed in the flat field vocabulary |
frame_contract.py | NOT in _META_FIELDS ⇒ in EDITABLE_FRAME_FIELDS |
frame_contract.py | NOT a canonical-overlap field (FORUM_OVERLAP_FIELDS = tribunal_name, forum_type; PARTY_OVERLAP_FIELD = parties) |
FrameOutput model
(_validated_frame → FrameMirror.model_validate), so a
string[] body for governing_law is accepted exactly as
key_instruments is today. Unlike relief_sought (Story-2360), there is
not even a canonical-overlap nuance to reason around: governing law is frame-native, with no
competing canonical column.
| Before | After | |
|---|---|---|
| Markup | RailListFact — one line, items joined with " · ", one provenance glyph for the whole line |
EditableList — one row per act, each row its own editable surface with its own provenance marker |
| Empty state | the word "Not set", and no way past it | the "Add an act or statute" button IS the empty state |
| Write | none | onSaveList("governing_law") → the page's existing saveList → useAppEditMatterFrame |
Counsel clicks an act, the caret drops in, blur commits. Clearing an act's text removes the row — the row IS the value, so an empty row is no row. "Add an act or statute" opens a fresh row. Every commit sends the whole next array, because the frame edit contract is full-value-per-field.
useAppEditMatterFrame's onMutate patches the cached Brief,
onError restores the snapshot and toasts, onSettled reconciles with
server truth. Zero new mutation code in this diff.
EditableSurface marker
via itemProvenance={(act) => fieldProvenance(brief, "governing_law")} — the
same call the old glyph made, now per row instead of per line. A canonical or locked value
stays un-removable; that gate lives in the primitive.
AnimatedList, which
EditableList already wraps.
addLaw and
lawPlaceholder. The primitive holds no copy; the caller resolves every label.
RailListFact was the read-only joined-line renderer, and
governing_law was its only caller. Promoting the field orphaned
it, so it is deleted in the same change rather than left to rot beside its
replacement (AGENTS.md, "Repo hygiene — no orphan files"). Net: the rail gained an editor and
lost a component.
The rail is dense on purpose — it is a reference card, not a form. The promoted block keeps the
rail's shape: the same FieldLabel eyebrow, the same mb-4 block
rhythm, the same Divider above and below, and the same
EditableList density Key instruments established two rows down. The only visual
delta is deliberate and is the point of the Story: acts stack as rows instead of running
together on one ·-joined line, which is what makes each one individually
correctable.
No new story. Governing law exercises states the
Shared/EditableList catalog already demonstrates — Populated, Empty/add-first, Add
flow, Remove, Provenance-marked, Rollback on error. No genuinely new state emerged, so no new
catalog entry was earned.
| Check | Result |
|---|---|
pnpm lint | 0 errors (3 pre-existing warnings, unrelated files) |
pnpm test | 189 files, 1237 tests, all passing |
pnpm build | clean |
git diff --check | clean |
FrameOutput model and the server-side edit validator
in pramaan-functions — not a live round-trip. No live staging write
of governing_law was performed from this branch, and a green build/test is not
deployed-runtime proof (AGENTS.md LI-7). Staging UAT — add, edit, remove an act, plus the empty
first-run state on an unframed matter — runs post-merge on stg.app.pramaan.io.
4 file(s), +21 / -33 lines
| Path | Role | + | - |
|---|---|---|---|
| messages/en.json | Config | +2 | -0 |
| messages/hi.json | Config | +2 | -0 |
| src/components/matter-detail/brief/matter-brief-page.tsx | Component | +1 | -1 |
| src/components/matter-detail/brief/matter-brief-sections.tsx | Component | +16 | -32 |