Spec §Group (Bootstrap) states unconditionally: "The creator of a _group Record is automatically stamped as its first admin." But stampGroupAdmin runs only in ScopedStack.create() (stack.ts:1953-1956, 1656-1663). A _group created via plain Stack gets an empty roster.
Consequence: management still works (the owner is always a manager via isGroupManager's owner check), but a { access: 'group', groupId, role: 'admin' } ACL entry pointing at an owner-created group matches no one (empty admin roster), and the owner must hand-add the first admin before role-gated group ACLs mean anything.
Decided: stamp the creator on the unscoped path too
Make "creator is first admin" a uniform invariant across both paths. Recommended implementation — move the stamping into Stack.create(), keyed on the record's author, rather than adding a second stamping site:
- In
Stack.create(), for a _group (by baseId), stamp opts.entityId ?? ownerEntityId as an admin roster association (idempotent, via the existing stampGroupAdmin helper).
- Remove the stamping from
ScopedStack.create() — it delegates to Stack.create(), which now handles it.
This resolves cleanly for every path because opts.entityId already carries the correct author:
Doing it this way avoids the double-stamp / owner-in-every-roster hazard that a naive "also stamp owner in Stack.create" would cause when ScopedStack delegates.
Work items
Refs
#58 (group roles / admin bootstrap), #69 (owner-writes-carry-no-entityId — why the owner path has no entityId to key on). From docs/design-assessment-2026-07.md §F2.
Spec §Group (Bootstrap) states unconditionally: "The creator of a
_groupRecord is automatically stamped as its firstadmin." ButstampGroupAdminruns only inScopedStack.create()(stack.ts:1953-1956,1656-1663). A_groupcreated via plainStackgets an empty roster.Consequence: management still works (the owner is always a manager via
isGroupManager's owner check), but a{ access: 'group', groupId, role: 'admin' }ACL entry pointing at an owner-created group matches no one (empty admin roster), and the owner must hand-add the first admin before role-gated group ACLs mean anything.Decided: stamp the creator on the unscoped path too
Make "creator is first admin" a uniform invariant across both paths. Recommended implementation — move the stamping into
Stack.create(), keyed on the record's author, rather than adding a second stamping site:Stack.create(), for a_group(by baseId), stampopts.entityId ?? ownerEntityIdas anadminroster association (idempotent, via the existingstampGroupAdminhelper).ScopedStack.create()— it delegates toStack.create(), which now handles it.This resolves cleanly for every path because
opts.entityIdalready carries the correct author:Stack(owner) →entityIdundefined → stampsownerEntityId. ✓ScopedStackowner (normalized to noentityIdper Polish batch: owner entityId stamping, unused timezone, lenient date validation, silent null content filter #69) → stampsownerEntityId. ✓ScopedStacknon-owner →entityId= requester → stamps the requester, not the owner (avoids leaking the owner's DID into every group roster). ✓Doing it this way avoids the double-stamp / owner-in-every-roster hazard that a naive "also stamp owner in
Stack.create" would cause whenScopedStackdelegates.Work items
_groupadmin stamping intoStack.create(), keyed onopts.entityId ?? ownerEntityId; drop it fromScopedStack.create()Stackhas the owner as first admin; non-owner group viaScopedStackhas the requester (and not the owner) as admin; owner-via-ScopedStackstamps the owner exactly onceRefs
#58 (group roles / admin bootstrap), #69 (owner-writes-carry-no-entityId — why the owner path has no
entityIdto key on). Fromdocs/design-assessment-2026-07.md§F2.