[Story-1177] Matter data-model v3 refactor + staging purge

Story 1177Epic None54 files · +4910/−5282

PR #301 · pramaan-io/pramaan-functions · merged 2026-06-10

Why this matters

A matter is not one court. But the old vocab pretended it was.

Story nine-eighty-eight — Matter data-model v3

One row, one vocab, one proceeding — the moment you add a second, you are stuck.

The fix — One matter, one frozen snapshot, one place for parties — the vocab lives with the proceeding it serves.

How it flows

sequenceDiagram
  autonumber
  participant matter as Matter
  participant main as Main
  participant appeal as Appeal
  participant snapshot as Snapshot
  matter->>snapshot: freeze template_snapshot
  matter->>main: seed filing types + labels
  matter-->>appeal: seed its own forms + labels
  main->>main: Statement of Claim ✓
  appeal->>appeal: Memorandum of Appeal ✓

matter/service.py + alembic 0090

Methodin → outwhat it does
_build_template_snapshot() → dict[str, Any]freezes the template at matter-create so the snapshot never drifts
_nest_template_vocab(table, source_column) → Nonenests filing types and labels under proceeding type so uniqueness is per proceeding
_write_partiesasync def _write_parties( session: AsyncSession, firm_id: UUID, matter: Matter, main_proceeding: Proceeding, """Write parties to matter_participants + firm labels on the Main proceeding.""" if not parties: our_count = 0 labels_by_name: dictwrites parties once to matter_participants and records firm labels on the Main proceeding
_assign_lead_counsel(session, firm_id, matter, user_id) → Noneassigns the opener as lead_counsel and enforces the at-least-one lead invariant

What changed, file by file

0090 migration that nests filing types and party labels under proceeding type, creates proceeding-scoped vocab tables, and repoints foreign keys.

new methodsignature
_create_updated_at_trigger(table_name) → None
_drop_updated_at_trigger(table_name) → None
_drop_policies(table) → None
_enable_matter_rls(table, matter_expression) → None
_add_staff_with_grant_select_policy(table) → None
_disable_rls_for_drop(table) → None
_create_proceeding_vocab_table(table, source_column, source_table) → None
_nest_template_vocab(table, source_column) → None
upgrade() → None
_recreate_matter_vocab_table(table, source_column, source_table) → None
_unnest_template_vocab(table, source_column) → None
downgrade() → None
New fields: revision : strdown_revision : str | Sequence[str] | Nonebranch_labels : str | Sequence[str] | Nonedepends_on : str | Sequence[str] | None

Matter create now writes the immutable template_snapshot, seeds matter_proceeding_types, sole-sources parties to matter_participants, and assigns the opener as lead_counsel.

new methodsignature
_side_to_role_type(role, our_index) → tuple[str, bool]
_write_partiesasync def _write_parties( session: AsyncSession, firm_id: UUID, matter: Matter, main_proceeding: Proceeding, """Write parties to matter_participants + firm labels on the Main proceeding.""" if not parties: our_count = 0 labels_by_name: dict
_assign_lead_counsel(session, firm_id, matter, user_id) → None
_party_views(session, matter) → list[MatterPartyView]
_party_count(session, firm_id, matter_id) → int
_template_name(matter) → str | None
_matter_type_display(matter) → str
_list_item(matter, client_name, party_count) → MatterListItem
_party_counts(session, firm_id, matter_ids) → dict[UUID, int]
_main_proceeding(session, firm_id, matter) → Proceeding
_build_template_snapshot() → dict[str, Any]
_seed_matter_proceeding_types(session, firm_id, matter, snapshot) → None

New SQLAlchemy models ProceedingFilingType and ProceedingParticipantLabel scoped by proceeding_id, with the same RLS and updated_at trigger pattern as migration 0085.

new classsignature
ProceedingFilingType(class)
ProceedingParticipantLabel(class)
New fields: id : UUIDfirm_id : UUIDmatter_id : UUIDproceeding_id : UUIDsource_filing_type_id : UUID | Nonename : strdescription : str | Nonestatus : strdisplay_order : intsource_participant_label_id : UUID | None

Proceeding create now seeds its own proceeding_filing_types and proceeding_participant_labels from the snapshot's block for that proceeding type.

new methodsignature
_seed_proceeding_vocab(session, firm_id, matter, proceeding, proceeding_type) → None
_filing_view(row, filing_type) → FilingView

Matter entity gains template_snapshot jsonb and matter_frame_text, and the assignment gains team_role; type, parties, and matter_template_name columns are dropped.

New fields: matter_template_id : UUID | Nonematter_frame_text : str | Noneteam_role : str

New shared constants MatterPartySide (our/their/neutral) and MatterTeamRole (lead_counsel/member) that drive party role mapping and assignment team_role.

new classsignature
MatterPartySide(class)
MatterTeamRole(class)

matter_template_filing_types and matter_template_participant_labels now carry a NOT NULL proceeding_type_id foreign key, so template uniqueness nests per proceeding type.

New fields: proceeding_type_id : UUID

Snapshot DTO reshaped to nest filing types and party labels under each proceeding type, exposed as templateSnapshot in the matter snapshot view.

New fields: matter_template_id : UUID | Nonetemplate_name : str | None

Matter DTO gains matter_frame_text to carry the human-readable matter frame on read.

New fields: matter_frame_text : str | None

Files changed (54)

File±
alembic/env.py+6 −3
alembic/versions/a1b2c3d4e5f6_0090_matter_model_v3_refactor.py ★+470
docs/walkthroughs/story-1177-walkthrough.html+154
openapi/app-api.json ★+2973 −4029
scripts/purge_staging_matters.sql+38
src/app_api/router_matters.py+0 −97
src/domains/filing_type/AGENTS.md+3 −3
src/domains/filing_type/service.py+4 −4
src/domains/matter/dtos/__init__.py+0 −14
src/domains/matter/dtos/matter.py+8
src/domains/matter/dtos/template_snapshot.py+22 −82
src/domains/matter/entity.py+42 −91
src/domains/matter/redesign_service.py+4 −357
src/domains/matter/service.py ★+377 −183
src/domains/matter_type/entity.py+24 −1
src/domains/matter_type/template_service.py+6 −56
src/domains/participant_label/AGENTS.md+3 −3
src/domains/participant_label/service.py+4 −4
src/domains/proceeding/entity.py+122
src/domains/proceeding/service.py+115 −34
src/shared/audit/coverage.py+0 −5
src/shared/constants/__init__.py+4
src/shared/constants/firm_status.py+25
src/shared/db/entities.py+8 −5
tests/factories.py+3 −3
tests/integration/test_agent_approval_request_rls.py+2 −2
tests/integration/test_agent_run_rls.py+2 −2
tests/integration/test_app_adversary_reads.py+4 −5
tests/integration/test_app_annotations.py+3 −5
tests/integration/test_app_audit_query.py+4 −5
tests/integration/test_app_claim_block_refs.py+2 −5
tests/integration/test_app_claim_rbr_links.py+3 −6
tests/integration/test_app_claims.py+3 −6
tests/integration/test_app_clients.py+2 −2
tests/integration/test_app_document_blocks.py+2 −5
tests/integration/test_app_documents.py+3 −5
tests/integration/test_app_filing_types.py+29 −3
tests/integration/test_app_matter_redesign.py+106 −108
tests/integration/test_app_matter_templates_update.py+10 −27
tests/integration/test_app_matters.py+4 −9
tests/integration/test_app_participant_labels.py+29 −3
tests/integration/test_app_rbrs.py+3 −5
tests/integration/test_app_validation_checks.py+2 −5
tests/integration/test_audit_reused_action_routes.py+32 −24
tests/integration/test_binder_cache_rls.py+2 −2
tests/integration/test_extraction_claim_promotion.py+3 −4
tests/integration/test_extraction_proposals_rls.py+2 −2
tests/integration/test_rls_isolation.py+22 −19
tests/integration/test_updated_at_triggers.py+4 −4
tests/routes/test_admin_firms.py+2 −2
tests/services/test_firm_service.py+2 −2
tests/shared/matter_model.py+83 −30
tests/unit/test_matter_model_v3_migration.py+90
tests/unit/test_matter_redesign_dtos.py+40 −11