Story-1531 · Matter Detail · FN-1 Filing-By / Party dimension

The Record now knows whose filing it is — participant.side + filing.filed_by_participant_id

Why this exists (the gap it closes)

The Record shows all matter material — our filings, the opponent's, and the court's. Until now the register surfaced what (filing type) and state, but never whose filing it is. A counsel reading the full record could not answer "show me everything the other side filed" or "everything from Vega Capital". The 2026-06-21 Filing-By decision fixes that by adding two small fields to the existing model — no new participant entity (pramaan-docs ADR-1179 is reused, not rebuilt).

Two structural facts, captured once

What changes

FileChange
src/shared/constants/firm_status.pyNew ParticipantSide StrEnum: ours / opposing / court / others. Distinct from the wizard bucket MatterPartySide (which maps to role_type).
src/domains/matter/entity.py — MatterParticipantside column (String(32), server_default 'others', NOT NULL) + CHECK matter_participants_side_valid.
src/domains/matter/entity.py — Filingfiled_by_participant_id (nullable Uuid) + composite FK (firm_id, matter_id, filed_by_participant_id) → matter_participants (RESTRICT, MATCH SIMPLE so NULL filers are unconstrained) + partial index ix_filings_firm_filed_by.
alembic/.../0115_participant_side_filing_filed_by.pyAdditive migration off head 0114 (d4a7c1e9b305). Single head; column comments byte-identical to the models so alembic check is clean. downgrade() drops both.
src/domains/matter/dtos/identity.pyside on MatterParticipantCreateRequest (default others), MatterParticipantUpdateRequest (optional), and MatterParticipantView.
src/domains/matter/identity_service.pyCreate sets side; update reclassifies it; view + audit metadata + diff/summary snapshots carry it.
src/domains/proceeding/dtos/proceeding.pyfiled_by_participant_id on FilingCreateRequest + FilingView.
src/domains/proceeding/service.pycreate_filing validates the filer is in this matter (reuses _active_participant) and records it; _filing_view echoes it.
openapi/app-api.jsonRegenerated: ParticipantSide schema, side, and filedByParticipantId on the participant + filing contracts. admin-api.json unchanged.
tests/integration/test_app_filing_by_party.pySystem-boundary coverage (real requests, real DB).

Why no back-compat / destructive is fine

Pre-launch (per the decision). side ships with server_default 'others', so every existing participant row is classified the moment the column lands — no zero/NULL state. filed_by_participant_id is nullable by design (most filings start unattributed).

Verification — done at the system boundary

single alembic headround-trip greenalembic check cleanRLS firm-scopedopenapi regenerated1028 passed

The integration test drives real POST/GET/PATCH through the router against a freshly provisioned firm — including the zero-input first run (side omitted ⇒ others; no filer ⇒ null):

POST /app/matters/1/participants  {entityId, roleType:"opposing_adversarial_side", side:"opposing"}  → side == "opposing"
POST /app/matters/1/participants  {entityId, roleType:"witness"}  (side omitted)                       → side == "others"
PATCH /app/matters/1/participants/{id}  {side:"court"}                                                  → side == "court"
POST /app/matters/1/participants  {side:"plaintiff"}                                                    → 422 (out-of-enum)

POST /app/matters/1/proceedings/{pid}/filings  {filingTypeId, title, filedByParticipantId}  → filedByParticipantId echoed
POST /app/matters/1/proceedings/{pid}/filings  {filingTypeId, title}      (no filer)        → filedByParticipantId == null
POST /app/matters/1/proceedings/{pid}/filings  {filedByParticipantId: }      → rejected (4xx)

Out of scope (parked, not now)

FN-1 is the data spine only. The FILING-LINK derive extension (carrying filed_by → name + label + side into the register), the Record's "Filing By" column + Side badges + group/slice, page-sum summaries, and the record-wide Party/Source for loose documents are separate follow-ups on the same register.

Closes pramaan-io/pramaan-spec#1531.