basquetWi + New ticket
enamel ENAMEL-86

The admin-minimum guard's advisory lock is an UNFORCED control: its mutual-exclusion effect cannot be rehearsed rollback-only, and forcing it red needs a disposable DB or authorised committed DML on the sole live admin grant

Backlog high cecoder-enamel-cc

Split out of ENAMEL-81 after audit-general-ca BLOCK:ddf794d2, which falsified the containment claim in rehearsal arm (h) rather than its arithmetic. fn_guardEnamelAdminMinimum (enamel_085, draft) takes pg_advisory_xact_lock before counting the replacement pool, so two concurrent removals of two different durable admin rows are ordered and exactly one must be refused. THAT EFFECT HAS NEVER BEEN SEEN TO FAIL, and under the fleet rule a control not seen to fail is a green line rather than a control. WHY IT CANNOT BE FORCED RED IN THE MIGRATION'S OWN REHEARSAL: a rollback-only rehearsal and a committed cross-session race are mutually exclusive. Separate sessions cannot share one transaction, so if the winning session ROLLS BACK the loser sees the grant restored and should succeed - the red is unreachable - and if the winner COMMITS, no terminal rollback in another session can remove that mutation. Two drafts asserted a containment that does not exist (2f7c4d34 also had the pool arithmetic wrong, leaving the real admin standing so both removals passed either way). WHAT WOULD ACTUALLY SETTLE IT, and neither is mine to authorise: (1) a disposable database or clone, which Evolutiva does not currently use for rehearsal - preview envs are explicitly not used and local Postgres is barred for app data, so this needs a route decision rather than a command; or (2) explicitly authorised COMMITTED fixture DML on public.userGroups admin grants, which under the test-exercise-residue rule requires an exact id manifest declared BEFORE the exercise, a named consenting human as actor of record, an archive context, and a verified-removal readback including affected aggregates. That surface is the sole live enamel admin grant with zero headroom (see ENAMEL-84) - the riskiest possible fixture and the exact row the guard exists to protect - so the authorisation is a PM/Elazar call. WHAT IS COVERED MEANWHILE: enamel_085 arm (k) asserts from pg_locks that the advisory lock is ACQUIRED at run time (classid 4294967295, objid 2341498009, objsubid 1, ExclusiveLock, granted - measured, not derived from hashtext arithmetic), with its red leg deleting the perform line. That converts the lock from unmeasured to partially measured and explicitly does NOT evidence mutual exclusion: a lock taken by one session says nothing about what a second session sees. UNBLOCKS ON: a decision between route (1) and route (2). Not a blocker on enamel_085 itself, which ships with the limit stated in the file.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • coder-enamel-cc created · 1w ago
  • coder-enamel-cc note · 1w ago
    ROUTE CORRECTION, measured against the live schema and the guard's own predicate rather than recalled. pmmaster-evolutiva-cc routed this to a third option - 'the guard is per-group, so a throwaway synthetic group with two synthetic admin grants exercises mutual exclusion without going near the live grant'. THAT PREMISE IS FALSE ON TWO COUNTS and the option does not exist as described. (1) THE GUARD IS NOT PER-GROUP. fn_guardEnamelAdminMinimum's replacement count has NO groupId predicate: it joins public.groups on g.value = 'admin' with g.isActive is true and g.deletedAt is null, and filters ug.appKey = 'enamel', ug.isSynthetic is false, ug.deletedAt is null, ug.fromAt <= now(), ug.toAt is null, ug.id <> old.id. The pool is therefore every live admin-VALUED group's durable enamel grants taken together. Live-schema check on public.groups: the only relevant unique index is ux_groups_value_owner_live UNIQUE (value, coalesce(ownerAppKey,'')) WHERE deletedAt IS NULL - so a SECOND live group with value='admin' is permitted provided ownerAppKey differs, but it lands in the SAME pool rather than a separate one. A throwaway admin-valued group therefore ADDS to the pool; it does not isolate anything. (2) SYNTHETIC ROWS ARE INVISIBLE TO THIS GUARD BY CONSTRUCTION. The subject test returns early when old.isSynthetic is not false, and the count requires isSynthetic is false. Synthetic grants are skipped as subject AND excluded from the pool, so a synthetic fixture cannot exercise the guard at all - which is exactly why this exercise is residue-ruled: it must use NON-synthetic rows, i.e. ordinary domain rows. WHAT THE BOUNDARY ACTUALLY REQUIRES. The discriminating property is that of TWO concurrent removals exactly one is refused, which needs the pool to be exactly 2 with both members targeted. The live grant is in the pool permanently, so pool=2 means {live grant, one fixture grant} and both are the racers. THERE IS NO ARRANGEMENT THAT REACHES THE BOUNDARY WITHOUT THE LIVE GRANT AS ONE OF THE TWO. Adding more fixtures moves the pool to 3+ and reproduces the vacuous shape audit-general-ca already falsified twice (remove two, the third stands, both removals succeed under a correct lock and a broken one alike). Narrowing the guard's scope to make a fixture-only pool possible is banned on its own grounds: do not change production SQL to make a test arm red. THE ONE ROUTE THAT WORKS, and why its risk is lower than it reads: pool = {live grant, fixture grant} where the FIXTURE POINTS AT THE SAME HUMAN as the live grant, in a second admin-valued group distinguished by ownerAppKey. Race the two removals and assert exactly one refusal. The admin-minimum invariant is never violated at any instant, because whichever row survives is a valid durable enamel admin grant held by that same person - so the capability never leaves the human who holds it today, which is the property that makes this materially different from 'removing the live admin grant'. Whichever session wins, recovery is re-creating the removed row and then removing the survivor, in that order, since the guard permits removing the survivor only once the pool is back to 2. STILL OWED BEFORE ANYTHING IS CREATED, per the residue rule: the exact id manifest, the archive context/reason, the removal readback including affected aggregates, and the named consenting human as actor of record. The actor-of-record is Elazar's single yes/no and is the only remaining gate; the DML itself is db-lane, not mine (SELECT only).
  • coder-enamel-cc note · 1w ago
    RESIDUE MANIFEST, pre-declared per the test-exercise-residue rule. Route approved by pmmaster-evolutiva-cc (muwq4pryzqfr) as specified. Every shape below is MEASURED off the live schema 2026-10-06, and one part of my own approved proposal was WRONG and is corrected here. SUBJECT ROW (the live grant, the pool's permanent member): userGroups.id dd1edea1-e171-40fb-a031-39566d1125d1, userId 8157954d-7a79-48c2-a67b-87a37970d688 (elazar.pimentel@pensanta.com), groupId d2a96c96-4176-4cff-be36-7ff9acae51d6 (groups.value='admin', ownerAppKey NULL, isActive true), appKey enamel, isSynthetic false, fromAt 2026-09-22T17:34:14.95263Z, toAt null, deletedAt null. It is the ONLY row in the pool: one live admin group, one durable grant. CORRECTION TO THE APPROVED ROUTE - the fixture group's ownerAppKey is NOT free-form. I proposed 'a second admin-valued group distinguished by ownerAppKey'; the admissible value is exactly 'enamel' and nothing else. fn_forbidGroupAppKeyMismatch (BEFORE INSERT OR UPDATE OF groupId, appKey, read from prosrc) raises whenever the group's ownerAppKey is not null and differs from the grant's appKey, so any invented ownerAppKey refuses the fixture grant at INSERT; and ownerAppKey NULL collides with the live admin group on ux_groups_value_owner_live UNIQUE (value, coalesce(ownerAppKey,'')) WHERE deletedAt IS NULL. So the only creatable shape is groups(value='admin', ownerAppKey='enamel'), whose unique key ('admin','enamel') is measured FREE - the only admin-valued group today is ('admin',''). PREREQUISITE, measured not assumed: the LIVE trigger is still tg_userGroups_guardEnamelAdminMinimum BEFORE UPDATE OF toAt, deletedAt, appKey, groupId, isSynthetic - the pre-enamel_085 shape with NO DELETE arm. enamel_085 must be APPLIED before this exercise, or the race runs against a guard that does not fire on DELETE at all and the result would say nothing about the shipped control. TO BE CREATED (2 rows, ids are gen_random_uuid defaults so they must be CAPTURED AND RECORDED ON THIS ROW AT CREATION, not predicted): (1) one groups row, value='admin', ownerAppKey='enamel', isActive true; (2) one userGroups row, same userId 8157954d-7a79-48c2-a67b-87a37970d688, groupId = the new group, appKey='enamel', isSynthetic FALSE (synthetic is invisible to the guard by construction, which is the whole reason this is residue-ruled), fromAt <= now(), toAt null. BLAST RADIUS WHILE THEY EXIST, measured in app/src: src/lib/admin-scope.ts resolves administered appKeys by av."group" = 'admin' alone, so the fixture grant confers REAL enamel admin - intended, and it is the same human who already holds it, which is the property that makes this exercise safe rather than a privilege change; and src/app/(protected)/usuarios/[id]/grant-forms.tsx offers groups where ownerAppKey is null OR equals the appKey, so the fixture group becomes a VISIBLE, ASSIGNABLE admin group in the enamel UI until it is removed. That is the specific reason cleanup must remove the GROUP and not only the grant. ARCHIVE CONTEXT: 'ENAMEL-86 forced-red of the admin-minimum advisory lock mutual-exclusion control; fixture admin grant and fixture admin group created for one two-session race and removed in the same session'. REMOVAL READBACK: after the race and recovery, assert (a) exactly one durable enamel admin grant remains and its id is dd1edea1-e171-40fb-a031-39566d1125d1 - the ORIGINAL id if it was never the race's loser, or a NEW id recorded here if it was removed and re-created, which must be stated explicitly rather than glossed; (b) zero live groups with value='admin' and ownerAppKey='enamel'; (c) zero userGroups rows referencing the fixture group under any deletedAt state, or if soft-deleted, the exact ids listed here; (d) the affected aggregate - the count behind admin-scope's ADMIN_OF_ANY for userId 8157954d - back to its pre-exercise value of 1 appKey. RECOVERY ORDER IS LOAD-BEARING: re-create the removed row BEFORE removing the survivor, because the guard permits removing the survivor only once the pool is back to 2. OPEN GATE, the only one: the named consenting human as actor of record. The actor would be Elazar's own account acting on his own access. I cannot initiate to him; the question is in my CLI reply and pmmaster is putting it in theirs. DML is db-lane; I hold SELECT only and will not create these rows.
  • coder-enamel-cc progress · 6d ago
    GO received from pmmaster (mv2cbdjuikcm), Elazar on his behalf, 2026-10-10. Route: Supabase branching (mcp__supabase__create_branch), not route 1 (previously thought unavailable) or route 2 (live-grant DML). A fresh branch applies main's migrations but carries zero production data, so the 2-row admin pool is seeded entirely on the branch with a disposable fixture user -- no residue manifest, no actor-of-record needed, live grant dd1edea1... never touched. Dispatched execution to db-enamel-cc (standing DB-routing hold, db02 excluded): create branch, confirm enamel_085 applied, seed fixture, race two concurrent removals, assert exactly one refused via pg_locks, report PASS/BLOCK, delete branch.
task
1w ago by coder-enamel-cc
1w ago