Problem
#58 added role?: 'admin' to group permission entries; #61 made setPermissions() a versioned, no-op-detecting mutation (skip the write when the new set is deep-equal to the current one). The no-op detector predates the role field and never learned about it: permissionEqual (packages/core/src/stack.ts:1627-1637) compares access/groupId/read/write for a group entry but not role.
Consequence: tightening { access: 'group', groupId, read: true, write: true } to the same entry plus role: 'admin' is judged deep-equal → Stack.setPermissions() returns early (stack.ts:949) → the restriction is silently not applied, while the call reports success and does not bump version. A failed tightening of access with no signal — the worst direction for a permission bug to fail in. The reverse (removing role to widen from admins-only back to any-member) fails the same way.
The SQLite layer stores permissions as an opaque JSON blob and does no equality check of its own, so permissionEqual/permissionsEqual in core is the only place this is decided — the fix is localized.
Fix
Include role in the group branch of permissionEqual:
if (a.access === 'group' && b.access === 'group') {
return (
a.groupId === b.groupId &&
a.role === b.role && // <-- add
a.read === b.read &&
a.write === b.write
);
}
(The entity and public branches are already complete.) While here, confirm no other comparator/serializer predates role — associationEqual is unrelated, and the wire path serializes permissions verbatim, so this should be the only site.
Tests
Refs
#58 (introduced role), #61 (no-op-detecting setPermissions). From docs/design-assessment-2026-07.md §A2 (PR #105).
Problem
#58 added
role?: 'admin'to group permission entries; #61 madesetPermissions()a versioned, no-op-detecting mutation (skip the write when the new set is deep-equal to the current one). The no-op detector predates therolefield and never learned about it:permissionEqual(packages/core/src/stack.ts:1627-1637) comparesaccess/groupId/read/writefor a group entry but notrole.Consequence: tightening
{ access: 'group', groupId, read: true, write: true }to the same entry plusrole: 'admin'is judged deep-equal →Stack.setPermissions()returns early (stack.ts:949) → the restriction is silently not applied, while the call reports success and does not bumpversion. A failed tightening of access with no signal — the worst direction for a permission bug to fail in. The reverse (removingroleto widen from admins-only back to any-member) fails the same way.The SQLite layer stores
permissionsas an opaque JSON blob and does no equality check of its own, sopermissionEqual/permissionsEqualin core is the only place this is decided — the fix is localized.Fix
Include
rolein the group branch ofpermissionEqual:(The
entityandpublicbranches are already complete.) While here, confirm no other comparator/serializer predatesrole—associationEqualis unrelated, and the wire path serializes permissions verbatim, so this should be the only site.Tests
role: 'admin'to an existing group entry (otherwise identical) actually persists and bumpsversion— regression for the silent no-oprole(widening admins-only → any-member) likewise persistsrole) still correctly no-ops (no spurious version bump)Refs
#58 (introduced
role), #61 (no-op-detectingsetPermissions). Fromdocs/design-assessment-2026-07.md§A2 (PR #105).