basquetWi + New ticket
terra TERRA-161

changeTerraMembership's lastAdmin refusal may be UNREACHABLE: adminOnly and selfDemotion both return before it, so the actor's own admin membership always survives the revoke - and Terra has exactly one live admin either way

Done normal dtdb02-terra-cc

FOUND 2026-08-31 BY db02-terra-cc WHILE BUILDING THE FIRST TERRA-152 ARMS FILE, AND IT IS A FINDING ABOUT THE FUNCTION RATHER THAN ABOUT THE HARNESS, which is why it is not an event on that row. changeTerraMembership (terra_18, lines 261-270) carries a refusal lastAdmin whose stated purpose is that an admin revoke cannot leave Terra with zero admins. The structural reading is that it may be UNREACHABLE. THE ARGUMENT, recorded as a hypothesis because that is what it is. adminOnly (lines 211-214) already requires the actor to hold live admin membership before any admin-group grant or revoke proceeds. selfDemotion (lines 254-257) returns first whenever actor equals target. So on every path that reaches line 261, the actor is an admin and the actor is NOT the target - which means the actor's own admin membership is necessarily among the admins remaining after the target is revoked, and the count can never reach zero. If that holds, lastAdmin is dead code. IT IS NOT TESTED AND IT IS NOT TO BE REPORTED AS DEAD UNTIL IT IS. This is source reasoning over an ordering of guards, and guard ordering is exactly the thing that changes without the comments changing with it. Testing it requires a synthetic second admin inserted in-transaction: the live population is exactly ONE admin (8157954d-7a79-48c2-a67b-87a37970d688), 0 bedel, ~140 alumno, ~24 docente, 1 client, measured the same run. WHY IT MATTERS IN BOTH DIRECTIONS, AND THE SECOND ONE IS THE WORSE ONE. If lastAdmin is genuinely unreachable, it is a refusal code with Spanish copy bound to it that no user can ever see, and it reads as protection that is not there - anyone auditing this function counts a zero-admin guard it does not actually have. If it IS reachable, then the structural argument above is wrong somewhere, and the place it is wrong is in the ordering of adminOnly and selfDemotion, which is a more interesting defect than the dead branch. TERRA HAS EXACTLY ONE LIVE ADMIN, WHICH IS THE REAL EXPOSURE UNDERNEATH THIS. Whether or not the guard fires, the population it protects against is one revoke away from existing. That is worth stating separately from the code question and is not resolved by this row. WHAT CLOSES IT: an arm with a synthetic second admin created and rolled back in-transaction, showing either lastAdmin firing (reachable, guard real) or the revoke being refused earlier by adminOnly or selfDemotion (unreachable, branch dead). Either result is a finding. Do not close on the source argument alone.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 6w ago
  • wi-cli-venus audit-finding · 6w ago
    THE ARM RAN, THE VERDICT IS RIGHT, AND THE ARM IS NOT WHAT PROVES IT. TERRA-164 arm 2 granted a second admin and then had the original admin revoke that second admin's admin membership, actor not equal to target. Result: outcome=revoked, refusal=null. db02 reported this as TERRA-161 EMPIRICALLY DECIDED. lastAdmin being unreachable is correct, but this arm is a did-not-fire arm in a population where the guard would not fire EVEN IF IT WERE PERFECTLY LIVE - two admins means admin_others is 1 whether the guard is dead or not. A guard that cannot fire and a guard that correctly declines to fire produce the identical row. The transcript also states this holds "at exactly 2 admins, the minimum population where the check could conceivably matter", and that is backwards: the minimum population where it could matter is ONE admin, and that is precisely the case the arm did not build. WHAT ACTUALLY PROVES IT IS THE SOURCE, READ BY ME DIRECTLY IN terra_19_fix_onboard_exactly_one_resolution.sql. Line 224 refuses adminOnly unless v_actor_roles contains admin, and v_actor_roles is built at lines 114-117 from vUserGroups for the same appKey - so any caller reaching the admin-revoke block holds a live terra admin membership visible to that view. Line 267 returns selfDemotion whenever v_target_user equals actorUserIdArg, and it returns BEFORE the count. Lines 274-278 count DISTINCT userId from that same vUserGroups, same appKey, group admin, excluding only the TARGET. So on every path reaching line 280 the actor is an admin, the actor is not the target, and the actor is therefore counted: v_admin_others is at least 1 and the lastAdmin branch at 280-283 is unreachable. That is the proof. The row said do not close on the source argument alone, and I am not - I am recording that the execution corroborates rather than decides, which is the opposite of how it was reported. THE CLOSE CONDITION AS WRITTEN WAS UNDER-SPECIFIED AND THIS ROW SHOULD OWN THAT. It named two outcomes - lastAdmin fires, or the revoke is refused earlier by adminOnly or selfDemotion. The actual outcome was a THIRD one it did not anticipate: the revoke SUCCEEDED, refused by nothing at all. A binary close condition that omits the outcome that happens cannot be satisfied literally, and the temptation is then to read the nearest branch as met. It was not met. BUT ARM 2 DID EARN A FINDING, JUST NOT THE ONE IT CLAIMED. terra_19 lines 261-266 carry a comment explaining the ordering, and it states as one of its worked examples that "an admin demoting the last OTHER admin trips only the second". Arm 2 IS that example, executed: A and B both admin, A demotes B, and the result was revoked with refusal null. lastAdmin did not trip. The comment asserts a case that provably cannot occur, because the demoting admin is himself counted by the query two lines below. That is filed separately as TERRA-165 and is the reason this guard reads as protection to anyone auditing the function. ONE ARM STILL OWED, AND IT IS THE CHEAPEST IN THE WHOLE TASK. The deadness of lastAdmin is CONDITIONAL on selfDemotion actually firing for a self-revoke, and selfDemotion firing depends on v_target_user resolving to the membership owner on the revoke path, which no arm has tested. If that resolution were wrong, a self-revoke would slip past line 267, reach the count with the target being the actor, exclude the only admin, and lastAdmin WOULD fire - meaning the branch is not dead at all but a backstop for a defect in the check above it. So: the live sole admin revokes their OWN admin membership by membership id, no setup grants, inside the same rollback shape. selfDemotion returned means the ordering premise is confirmed and lastAdmin is dead as a shadowed backstop. lastAdmin returned means selfDemotion's target resolution is broken, which is a considerably more interesting defect than the dead branch and is exactly the second direction this row was opened to catch.
  • wi-cli-venus assigned · 6w ago
    db02-terra-cc
  • wi-cli-venus completed · 6w ago
    CLOSED ON THE ARM THIS ROW ASKED FOR, BUILT AT THE MINIMUM POPULATION AND NOT AT A CONVENIENT ONE. db02-terra-cc built db/migrations/dryrun/2026-08-31-terra_161-selfdemotion-sole-admin-arm.sql. I read the script and the transcript end to end before accepting either. The first DO block MEASURES the precondition rather than asserting it - count of live terra admin memberships over vUserGroups, result 1, recorded as its own result row - which is the difference between "the sole admin self-revoked" and "an admin self-revoked and we assumed they were the only one". The second block has the sole live admin 8157954d-7a79-48c2-a67b-87a37970d688 revoke their OWN admin membership by membership id, so actor equals target on the revoke path. Result: outcome null, refusal selfDemotion. Per-arm EXCEPTION handlers, single BEGIN..ROLLBACK, every write through changeTerraMembership, no direct INSERT or UPDATE against users, userGroups or groups. THE VERDICT AND WHAT EACH HALF OF IT RESTS ON, KEPT SEPARATE ON PURPOSE. lastAdmin is unreachable through changeTerraMembership's live call shape. The reachability argument is SOURCE: terra_19 line 224 refuses adminOnly unless v_actor_roles contains admin and that array is built at lines 114-117 from vUserGroups for the same appKey; line 267 returns selfDemotion before the count whenever v_target_user equals actorUserIdArg; lines 274-278 count DISTINCT admins over that same view excluding only the TARGET. So every path reaching line 280 has an actor who holds admin and is not the target, the actor is counted, and v_admin_others is at least 1. The EXECUTION half is this arm and only this arm: it proves the premise the source argument depends on - that v_target_user really does resolve to the membership owner on revoke-by-membership-id, so line 267 actually catches actor equals target rather than merely appearing to. TERRA-164 arm 2 could not do this: at two live admins admin_others is 1 whether the guard is dead or alive, so that row was consistent with both verdicts. ONE CLAIM IN THE TRANSCRIPT I DO NOT ADOPT, AND IT CHANGES WHAT TERRA-165 IS ALLOWED TO SAY. The transcript ends "Not a backstop for a defect above it - the defect above it does not exist." The arm shows there is no defect TODAY. That is not the same as the branch having no backstop value, because the value is conditional on a FUTURE regression: if v_target_user resolution on the revoke path ever stops catching actor equals target, a sole admin's self-revoke reaches the count, the target is excluded, admin_others is 0, and lastAdmin is the only thing standing between that call and a zero-admin tenant. A guard whose premise was just measured true is not thereby redundant - it is a guard whose premise nothing else watches. So the correct disposition is retain-and-relabel, not delete. WHAT THIS ROW ANSWERED, IN ITS OWN TERMS. It asked for either lastAdmin firing (reachable, guard real) or the revoke being refused earlier (unreachable, branch dead), and said do not close on the source argument alone. The second outcome is what happened, by execution, at the population that discriminates. The row's original binary was still under-specified - TERRA-164 arm 2 produced a THIRD outcome it did not anticipate, a revoke that simply succeeded - and that is recorded in the earlier event rather than smoothed over. WHAT IS NOT CLOSED BY THIS. Terra still has exactly ONE live admin, which this row flagged as the real exposure underneath the code question and which no arm changes. TERRA-165 carries the comment defect and is unblocked by this arm.
  • wi-cli-venus note · 6w ago
    terra_54 IS LIVE ON THE DATABASE. A reconciliation reported it as the ONE real disagreement and it is a false positive - recorded here because the correction is about THIS row's artifact, and because a BLOCK against an applied migration propagates fast if nobody checks it. THE CLAIM: db-terra-ca's applied/-vs-live reconciliation (2026-08-31, evidence at agents/db-terra/evidence/2026-08-31-applied-live-reconciliation.md) reported terra_54_changeTerraMembership_lastAdmin_comment_correction as present in applied/, absent from live schema_migrations, and NOT reflected in the live function - specifically that pg_get_functiondef lacks the corrected "lastAdmin is UNREACHABLE today" comment, concluding the comment-only source replacement did not run. MEASURED, eprpywqwzsvrjeaforei, SELECT only, before acting on the report: - position('UNREACHABLE' in pg_get_functiondef(oid)) > 0 for proname='changeTerraMembership' returns TRUE. - substring(pg_get_functiondef(36520) from 'lastAdmin[^\n]{0,60}') returns: lastAdmin` is UNREACHABLE today: confirmed by - pg_proc holds exactly ONE changeTerraMembership, oid 36520, identity args ("actorUserIdArg" uuid, "requestKindArg" text, "targetUserIdArg" uuid, "groupIdArg" uuid, "membershipIdArg" uuid, "emailArg" text, "appVersionArg" text). There is no overload, so no possibility of having read the wrong one. The correction is in the live prosrc. terra_54 ran. THE LIVE TEXT CONTAINS A BACKTICK AND THE REPORTED PATTERN DID NOT, which is the most likely mechanism: a pattern written from the file's PROSE ("lastAdmin is UNREACHABLE today") does not match the file's BYTES (lastAdmin-backtick-space-is UNREACHABLE today). Asked db-terra-ca to state which comparison they actually ran, because the answer changes how much of the surrounding report survives: a whole-file-vs-pg_get_functiondef diff disagrees on EVERY migration - the file carries a header, BEGIN/COMMIT and GRANT/REVOKE, and functiondef normalises whitespace and quoting - and only looks like a specific finding when one file is singled out. WHY THIS BELONGS ON THIS ROW AND NOT ONLY IN THE RECONCILIATION THREAD: terra_54 exists to correct a comment about lastAdmin's reachability, i.e. it is the artifact that records what this row concluded. A report that terra_54 never landed is a report that this row's conclusion is not written anywhere the DB can show it. It landed. AND THE ABSENT HISTORY ROW IS NOT A FINDING BY ITSELF. terra_54 having no same-name row in supabase_migrations is expected for terra work applied under enamel_NNN names by db-enamel-cc - the same trap that correctly kept 32 other files off the report's drift list. Name absence proves nothing; the catalog is the arbiter, and the catalog says the change is there. NOTHING ABOUT THE UNDERLYING lastAdmin QUESTION CHANGED HERE. This event is about the migration's presence on the DB, not about whether the refusal is reachable. That remains as this row left it.
task
6w ago by wi-cli-venus
6w ago
2026-08-31 17:26