Story-3392 · pramaan-functions · RFD-MATTER-006 DL28 · Lane A · after F3/F4/F5

The matter nobody can reach

An entry gate with no exception turns "everyone left the team" into "this matter is gone". Story-2677 refuses an unstaffed editor the matter-detail page and tells them to ask someone on the team. Story-3389 then made teams empty out on their own — suspend or demote the last two people and the roster is empty while the matter is untouched. At that point the advice has nobody to address: every editor is refused, and the remedy is a firm admin. DL28 is the exception that lets the firm recover its own work.

Orphan means EMPTY, not "no lead"

The definition is strict and it matters: an orphan is a matter with zero business.matter_assignments rows — no lead and no members. A matter that has merely lost its lead still has people on it who can act, and the accepted No-lead state (F3) is exactly that situation. Widening the exception to "no lead" would hand every editor the door to a matter that already has a team, which is Story-2677's containment deleted rather than qualified.

Because the definition is load-bearing in two places, it is written once. The policy in migration 0254 and the application gate both ask business.matter_team_is_empty(firm_id, matter_id). A gate that opened the door on one definition while the policy admitted writes on another would be a door onto a wall.

Entry alone would have been a dead end

The ticket scoped this as an entry-gate change, with the adopt actions already delivered by F3/F4. They are — for staffed users. Every adopt action (claim lead, assign someone, add self) routes through the team:staff capability, and that capability requires an assignment row on the exact matter, which an orphan by definition has nobody holding. Opening only the entry gate would have delivered a page whose every button returns 403. So the same exception reaches three layers, and stops there:

LayerWhat changedWhat deliberately did not
_require_matter_detail_entry An active firm_editor may ENTER a true orphan, and the open is audited. System reads, the admin bypass, staffed users, and the matters LIST are all untouched.
_require_matter_capability_access team:staff on a true orphan for an active editor. content:write stays shut — an editor cannot edit an orphan's content until they have actually adopted it.
matter_assignment_insert_policy (0254) INSERT admits the same case, via the shared helper. UPDATE and DELETE keep the 0192 predicate verbatim — an orphan has no rows to update or delete.

The grant that had to travel with the code

Migration 0192 made the team-write policies "pure lateral reach": firm admin, or an existing assignment row on that matter. That is the database twin of the application staffing floor, and it is why this could not be an application change alone. With the app layers opened and the policy untouched, the adopt path did not fail politely — it 500'd:

PUT /app/matters/2/lead → 500
asyncpg.exceptions.InsufficientPrivilegeError:
  new row violates row-level security policy for table "matter_assignments"
[SQL: INSERT INTO business.matter_assignments (... team_role ...) VALUES (...)]

That is R2 as a measurement rather than a maxim, and it is this change's observed-red evidence: the integration test below was written first, run against the two application layers alone, and reported exactly this. The migration is what turns it green. The clause is also self-extinguishing — the helper is STABLE, so it runs on the statement snapshot and answers "was this team empty before the write". The first adopted row makes the matter non-empty, and every write after it goes back through lateral reach.

Where DL28 narrows an existing guarantee, on the record

Story-3388 shipped an invariant that a removed editor cannot re-insert themselves — and its test built the removal by taking the only member off, which under DL28 is an orphan. The two decisions genuinely collide in exactly one case, and DL28 is the later and more specific one: once the roster is empty the removed editor is not a special principal, they are an active firm editor like any other, and the alternative is a matter no human below firm_admin can ever reach.

Rather than delete the guarantee or leave a stale assertion, test_removed_opening_editor_cannot_reinsert_assignment now pins both sides of the boundary: the matter keeps a colleague, the removed editor's INSERT is still refused by RLS, and only after the last member is taken off does the identical INSERT succeed.

Scenarios, and the one that is stronger than specified

ScenarioResult
S48admin opens an orphan200 — pre-existing bypass, unchanged
S49any active firm_editor opens an ORPHAN200 — the exception, audited
S50editor claims the vacant lead on an orphan200, roster = that editor as lead_counsel
S51editor (and admin) seats someone else on an orphan200, actor left off-roster and still gets the change back
S52editor adds self as member200, noLead: true — the accepted No-lead state
S53plain firm_member opens an orphan404 — see below
S55unstaffed editor opens a NON-orphan403 pramaan.matter.not_on_team

S53 is nominally a 403 and the system gives a 404. That is the pre-existing and stronger answer, not a miss: RLS (business.can_user_read_matter) admits a firm_member to a matter only through an assignment row, so an unstaffed member cannot see that the matter exists and the read 404s before the entry gate is consulted at all. pramaan.matter.not_on_team is by construction reachable only by a firm_editor, who alone sees every matter in the firm and is therefore the only principal the gate ever has to turn away. Asserting 403 there would have been asserting a weaker outcome than the one the system gives.

S55 is the mandatory regression and it is asserted first in the test, before any orphan is opened — an implementation that passed by opening everything would fail on line one rather than at the end.

Why a plain read is not the orphan check

The count comes from a SECURITY DEFINER helper, never a direct matter_assignments SELECT, and the reason is a trap rather than a style preference. That table's SELECT policy shows a firm_member only their OWN row. A direct count would therefore return zero on a fully staffed matter — reading as an orphan to precisely the caller who must not adopt one.

Green-signal sentence

This change asserts that a matter with an empty roster is enterable and adoptable by an active firm editor, at all three layers that would otherwise refuse it. It asserts nothing about a matter that still has anyone on it — S55 and the rewritten 3388 invariant both pin that the ordinary staffing floor is unchanged — and nothing about matter CONTENT, which stays behind content:write until the editor has actually adopted the matter.