Story-3390 · functions lane · RFD-MATTER-006 F6

Destructive matter operations are classified before they ship

A generated registry now discovers custody-reducing API operations from the mounted FastAPI application. Every matter-governed operation declares an authorization class; every destructive operation outside that boundary has an explicit disposition. CI regenerates the file and fails if a new route is unclassified or the committed snapshot is stale.

mounted DELETE / destructive POST / destructive PATCH semantic
                         ↓
       classified matter guard or explicit disposition
                         ↓
 openapi/destructive-operation-manifest.json + CI diff gate

The authorization rule

All 19 matter-governed operations currently use the staffed-editor class: a staffed firm editor, the active matter lead, or a firm admin may proceed. An ordinary staffed member receives 403. The guard reuses F1's active, same-matter staffing boundary and takes a row lock on the assignment while authorizing the change.

Generated matter-operation manifest

MethodOperationSelectorAuth class
DELETE/app/agent-sessions/{session_id}allstaffed-editor
POST/app/agent-sessions/{session_id}/archiveallstaffed-editor
POST/app/agent-sessions/{session_id}/clearallstaffed-editor
DELETE/app/documents/{document_id}/annotations/{annotation_id}allstaffed-editor
PATCH/app/matters/{matter_id}status=archivedstaffed-editor
PATCH/app/matters/{matter_id}/adversary-reads/{read_id}/statestate=archivedstaffed-editor
DELETE/app/matters/{matter_id}/claims/{claim_id}allstaffed-editor
DELETE/app/matters/{matter_id}/claims/{claim_id}/block-refs/{ref_id}allstaffed-editor
DELETE/app/matters/{matter_id}/claims/{claim_id}/rbr-links/{rbr_id}allstaffed-editor
POST/app/matters/{matter_id}/document-relationships/{relationship_id}/revokeallstaffed-editor
DELETE/app/matters/{matter_id}/document-saved-views/{view_id}allstaffed-editor
DELETE/app/matters/{matter_id}/documentsallstaffed-editor
DELETE/app/matters/{matter_id}/documents/{doc_id}allstaffed-editor
POST/app/matters/{matter_id}/documents/{doc_id}/collation/undoallstaffed-editor
DELETE/app/matters/{matter_id}/documents/{doc_id}/legal-holdallstaffed-editor
DELETE/app/matters/{matter_id}/frame/locks/{field}allstaffed-editor
DELETE/app/matters/{matter_id}/participants/{participant_id}allstaffed-editor
PATCH/app/matters/{matter_id}/proceeding-types/{matter_proceeding_type_id}status=archivedstaffed-editor
PATCH/app/matters/{matter_id}/proceedings/{proceeding_id}/stages/{stage_id}status=archivedstaffed-editor

Field-level and non-matter decisions

PATCH /app/matters/{matter_id} remains available for ordinary edits. The service checks status=archived after resolving the matter and before mutating any field, so a mixed title-and-archive request is rejected atomically. closed remains an ordinary visible lifecycle milestone; it does not hide the matter or reduce custody, so it is not classified as destructive.

Matter-bound chat sessions are matter-governed even though their routes are keyed by session ID: the dependency resolves the owned session's real matter_id before applying the guard. Document annotations use the same pattern and bind both annotation ID and document ID before authorization. Affiliation deletion is deliberately different under DL23: an affiliation is a firm-shared identity edge with no matter_id, so it keeps an explicit firm-editor-or-admin gate rather than borrowing authority from any one matter roster.

Red-first and behavioral proof

ScenarioEvidence
S13-S16Real PostgreSQL/RLS test admits a staffed editor, member-lead, and firm admin while rejecting a plain staffed member and an unstaffed member through the single named guard used by every manifest operation.
S17 / C4A member's mixed title + status=archived update raises 403 inside MatterService.update_matter; the title and status are unchanged afterward.
S18 / DL23A member staffed on matter A cannot delete a firm-shared affiliation; firm editor and firm admin requests succeed.
S19The live claim→RBR DELETE guard was temporarily removed. make gen-destructive-ops failed with that exact route in unclassified=[...]; restoring the guard returned the generator and snapshot test to green.
Inventory driftThe contract test compares the generated registry to the committed JSON, rejects unsupported classes, pins the archive selector, and plants an independent unclassified DELETE.