Phase 6 of docs/subgroups.md. `DkgRitualViewModel.createAdminGroup` was 120 lines
of room creation living in a view model, and a subgroup needs all of it with four
different values. It moves to `managers/MarmotGroupCreation`, unchanged in
behaviour, and the view model shrinks by 154 lines to the four values and a
`when` over the result.
The rules that were already right stay right for the subgroup for free, which is
the whole reason to move rather than to write a second one:
**Every key package before anything exists.** The id is derived, so there is
exactly one room per key at a path; a half-created one occupies that address
permanently and there is no second id to retry with. Better to create nothing and
name who is missing. (Phase 7 will check this at the picker instead, so a subgroup
does not discover it after three ceremonies -- this stays as the backstop.)
**`adminPubkeys` baked into the epoch-0 group context** rather than added by a
later commit, so a member welcomed afterwards gets a populated group instead of
chasing a bootstrap commit that predates their membership. That is why
`MarmotGroupData` is built by hand rather than through `MarmotGroupData.bootstrap`,
which hardcodes a single admin.
**`adopt` before the members are added.** Filing the key state is local and
certain; adding members is a relay round trip that can partly fail. The room comes
into existence already knowing what it signs with, whatever happens next.
**Derived ids make every step reachable twice**, so `Existing` is a success rather
than a refusal -- a second tap or another member getting there first should join
what exists rather than mint a rival group on one address.
Four outcomes instead of four scattered early returns: `Created` with the members
who could not be added, `Existing`, `BlockedOn` with the missing key packages, and
`Failed`. The view model keeps the one thing that was genuinely its own -- turning
missing public keys into names, since "no key package for 3 people" is not
actionable and "Bob needs to publish a key package" is.
**It is reached through `ChatRepository.createMarmotGroup`, not called directly.**
View models in this app talk to repositories and managers take the database; the
first cut had the view model reaching for `dkgRepository.database`, which does not
exist on the interface and should not. The repository method is three lines of
delegation and keeps the boundary where the rest of the app has it.
`parentChatRoomId` is the one genuinely new parameter, written onto the room after
`getOrCreateChatRoom` returns rather than passed into it -- that call is shared
with every other way a room appears and none of them has a parent to hand it.
Nine imports the extraction made dead are dropped from the view model. 397 common
tests, 701 jvm tests, `m3Audit` meets every budget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>