Matter Frame counsel edit, lock, and ratify API
Story-1998 turns the persisted Matter Frame into something counsel can safely correct, protect, accept, or reject without letting a later Vega pass overwrite counsel judgment.
The mental model: the Matter Frame is an append-only ledger. An edit writes a new page, a lock pins one field on the desk, and ratify decides whether a proposed value becomes counsel-owned truth or a suppressed proposal.
What changed
Counsel edit
PATCH /app/matters/{matter_id}/frame appends a new source='counsel' version and refreshes the derived matter_frame_text.
Field lock
PUT and DELETE on /frame/locks/{field} maintain business.matter_frame_field_locks with the pinned locked_value.
Ratify
accept locks the proposed value as counsel-owned. reject clears the field and pins the cleared value so Vega does not re-propose it.
Canonical handoff
Some frame fields are mirrors of canonical product state. Accepting tribunal_name or forum_type writes the Main proceeding's forum through the existing proceeding command path. Accepting parties creates a matter participant through the existing matter identity command path.
That is the ownership transfer: once counsel accepts a proposal, the canonical table owns the durable record and the frame keeps the counsel decision in its envelope and lock row.
Safety rails
- Every command resolves the matter through the firm-scoped app service path, so RLS remains the tenant boundary.
- Every frame write appends a
matter_framesversion; no prior version is mutated. - Every command emits audit activity, and appended versions enqueue
matter_frame.updatedthrough the domain-event outbox. - The integration tests call the real app routes against a freshly migrated test database, including lock refresh, canonical forum transfer, participant creation, and rejection suppression.
Verification: uv run ruff check, uv run mypy, route-level pytest, full pytest, and make openapi are the intended PR checks for this Story.