Story-3389 · functions lane · RFD-MATTER-006 F5 · DL2 / DL27 / DL30
A suspended lead stops holding the seat
Clerk owns the firm-membership lifecycle. Deactivate, suspend and
firm-removal all land in the two webhook mirror methods — and until this
story those methods flipped identity.firm_memberships and never
touched business.matter_assignments. A member Clerk had removed
from the firm still appeared on every matter team they were on, still held
lead counsel, and nothing said otherwise. The only path that ever freed a
seat was the Settings→Team admin action, which is not the path Clerk uses.
Clerk event ─→ apply_webhook_status_change / apply_webhook_role_change
│ (same transaction)
└─→ reconcile_membership_team_assignments
│
active member/editor/admin ─────────┴─→ no-op: the lead keeps the seat
inactive / firm-removed / support ──┴─→ every assignment removed
├─ was lead → matter No-lead
└─ was last → matter No-members
The other half: editor-only was the wrong question
The reconciliation kept a lead only when the resulting membership was an
active firm_editor. An active firm_member who led a
matter fell into a branch that demoted them to a plain teammate, and an
active firm_admin lead was removed outright. F2 had already
retired editor-only lead eligibility at create and assign (DL2: any active
member, editor or admin may lead), so the cascade was enforcing a rule the
rest of the domain no longer held. It is now one predicate, read by both the
preview and the realized reconciliation.
| Resulting membership | Matter team |
|---|---|
| Active member, editor or admin | Unchanged. A lead keeps the lead — a demotion inside the eligible set unseats nobody. |
| Deactivated / suspended | Every assignment removed. A vacated lead leaves the matter No-lead. |
| Firm-removed | Same, with membership_removal as the recorded reason. |
Moved to support | Same: the role cannot hold a matter seat. |
Vacating REMOVES the assignment rather than demoting it. Demotion was the old behaviour and it leaves an inactive user staffed on a live matter — the exact seat this rule exists to free.
The preview now says what will actually happen
Settings→Team shows a dry run before an admin deactivates someone. It used to offer an outcome claiming a replacement lead would be promoted into the vacated seat — a mechanism F3 retired, so the preview was describing work nobody does. It now returns only outcomes the reconciliation realizes, and it computes them from the same team sizes:
| Outcome | Meaning |
|---|---|
unchanged | The membership stays lead-eligible; the led matter is untouched. |
no_lead | The lead seat is vacated and stays vacant until a human fills it. |
no_members | The matter's last assignee is leaving. Outranks no_lead: an empty team has no lead either, and the emptier fact is the one an admin must act on. |
A plain member leaving a team that survives reports no consequence at all — the matter keeps its lead, and a warning there would train admins to click past warnings that mean nothing.
The seat is freed under the lock every other team writer takes
The reconciliation locks each matter row before re-reading the assignment
rows, so the role values it acts on were read under serialization rather than
carried across the gap. Removal goes through the security-definer door
business.remove_matter_team_assignment, the same one the roster
routes use. The remaining-team count that decides no_members is
read AFTER the removals and under those same locks, so it is the team the
matter actually has and not a pre-removal number arithmetic was applied to.
The webhook session carries staff context and no app.current_user_id,
so the reconciliation re-scopes itself to the affected firm exactly as the
channel offboard sweep already does. It runs inside the webhook transaction:
if it raises, the mirror flip rolls back with it and the message retries
rather than leaving the membership inactive and the seat occupied.
A distinct verb, because nobody performed this removal
The cascade emits matter.roster_vacated_membership_lifecycle,
not matter.removed. Reusing the removal verb would credit a
colleague with an action they never took and would hide the lifecycle cause
an admin needs to read next to a No-lead matter. The event carries the
previous team role, the resulting firm role and status, and which of the four
reasons freed the seat.
Proof
| Scenario | Evidence |
|---|---|
| S45 | A Clerk organizationMembership.deleted event takes the lead off the team, leaves the matter No-lead, and writes one lifecycle-vacancy audit row with a null actor. A replay writes no second row. |
| S46 | Suspended and deactivated memberships both free the seat; the last teammate going inactive empties the team. |
| S58 | A lead on two matters previews one No-lead consequence per matter. |
| S59 | Deactivating the matter's sole assignee previews No-members. |
| S60 | Deactivating a plain member previews and realizes no consequence; the lead stays seated. |
| S61 | No preview outcome promotes a replacement, asserted against the shipped enum so a future promotion outcome cannot be added silently. |
| DL2 regression | MT-12 and MT-14 were rewritten: an active editor-lead demoted to member keeps the lead, and the preview reports it unchanged. Both were red against the old branch. |
| Red-first | With the webhook wiring reverted, six of the seven new lifecycle proofs fail; the seventh is the control that must pass either way. |
One-sentence recall
Because Clerk is where deactivation actually happens, the mirror methods now run the same reconciliation the admin path runs — and because any active firm user may lead, only leaving that set frees the seat, which then stays vacant and says so.