The note said "add a parent group opens a picker of candidate rooms" and left the list unspecified, which is the easy place to design the wrong screen. It is not the groups that exist, and not the groups C's admins can name. **A certificate is proposed inside the adopting group, so the list is the selector's own rooms.** `FrostSigningManager.proposeSigning` runs in P2, so a candidate has to be a Marmot room this device already administers and holds a share of -- which is `SubgroupManager.refuseCeremonyRoom`'s first three checks with the target moved, plus C itself, C's existing parents and C's descendants removed. The admin test reads `MarmotGroupData.adminPubkeys` rather than `Participant.adminAt`, for the reason that function already gives: the flag is written only by `processGroupMembershipChanges` on an arriving commit, so on a room this device created it is null for everybody. So a group cannot be offered as a parent by somebody who is not in it. The overlap requirement the note already states turns up here as a list length, and an empty picker is a fact about the member rather than a failure -- the remedy is a person, not a button. **The filter costs one MLS decode per room and two indexed reads.** `toMlsGroup()` deserialises the whole group state out of hex to reach `adminPubkeys`. `canSign` is cheap for exactly the rooms that pass, because `completedKey`'s first branch is a key-state lookup and a session lookup by id; its `getKeyHoldingSessions` fallback, with a `marmotGroupId` derivation per session, fires only for rooms with no key state, which fail the filter anyway. Built the way `SelectChatRoomMembersViewModel.scheduleKeyPackageEventSynchronization` builds its prefetch -- off the main thread, filling in as it resolves. **Picking a parent is making an offer, not taking a decision.** The ordering is forced -- the key state carries the certificate -- so P2's quorum commits first and C's quorum answers second. Whoever picks P2 is not choosing on C's behalf; C's admins decide at step 2 by signing the re-announced key state or not. That is why the `ProposedEvent` 30326 arm is the most important small change in the design, and the note now says so from this direction too. **And it exposes a gap the founding flow does not have.** Founding has the same ordering -- `proposeBirthCertificate` runs before the child's quorum has signed anything -- but a subgroup's admins have at least *seen* the intent, because `DkgRitualEvents` puts `parent_group` on the ceremony proposal and the ritual screen names the group. Adoption has no ceremony and so no hint, which means the first thing an admin of C would hear is a request for their signature. `ChatMessage.applyInnerEvent`'s 30329 arm should grow a second branch for it. Today it calls `SubgroupManager.record`, which asks `certifies(event, subgroup, chatRoomId)` -- the certificate's parent must be the room it arrived in -- and drops everything else. A certificate that instead names *this* room as its subject, and verifies against the parent it names, is the arriving offer and gets its own line: "#Ekklesia has offered to adopt this group", beside today's "The group made #Translators a subgroup". Both branches are fully verified and neither needs a lookup, since `certifies` checks a signature against a room id and a room id is a public key. Nothing is stored -- `GroupSignedEvent`'s foreign key onto `ChatRoom` forbids it and nothing needs it, because the certificate reaches C's devices for real inside the key state at step 2. The line is notice, not evidence, and must read as an offer rather than as a fact about C's lineage. **Two entry points are defensible and they produce an identical certificate.** C's detail screen beside the parent card, mirroring the parent screen's "add subgroup"; or P2's beside that button, where the proposal actually lands. C's is the better default, because the intent originates with the group looking for a parent and its quorum is the one that decides. P2's is worth adding later, with the same guards read in the other direction and a candidate list of "Marmot rooms I hold a key state for", since that is where `subgroup_key` comes from. Two tests follow: the candidate filter in `SubgroupManagerJvmTest` -- a room this device is in but does not administer is not offered, and neither is one it administers but holds no share of -- and the new arm's branch in `ChatMessageJvmTest`. Sizing moves from six tests to eight and from one text file to two. Docs only -- no code changed, so there are no test counts to report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mantra docs
Notes on the parts of this app whose behaviour is not recoverable by reading the code alone — where the reasoning lives in a protocol, a failure mode that is silent, or a decision that looked arbitrary and was not.
| document | covers |
|---|---|
| shared-key-ceremony.md | ChillDKG over NIP-17: the rounds, the approval gates, the chat transcript, participant ordering |
| shared-key-derivation.md | deriving further keys from the group's threshold key with FROST tweaks — why not BIP32, why no chain code, and the one rule that must not be broken |
| subgroups.md | a group making another group — the four ceremonies, what the parent's signature actually covers, and why the child's key is fresh rather than derived |
| subgroups-multiple-parents.md | adding a parent to a group that already exists — two signings and no ceremony, why the child's consent is the whole gate, and the one screen that currently asks without saying what it is asking |
| frost-batch-signing.md | signing several events in one ceremony — why one nonce can never cover two messages, and the phased schema, wire and UI work that follows from it |
| marmot-membership.md | how members join an MLS group, and the epoch race that makes a missing member look like a successful invite |
| member-chronicle.md | handing a member added after the work was done the group's signed record — why the events are not on the wire at all, and why the room's id is enough to verify them |
| marmot-direct-messages.md | a one-to-one message inside a group as a stock NIP-59 gift wrap — what its MIP-03 carve-out costs, why the sender cannot read their own, and the one query that would broadcast it |
| mls-skipped-keys.md | why a group event that arrives a moment late is dropped for good, which flows trigger it, the quartz fix, and the partial mitigation in this app |
| long-running-sync.md | the chat subscriptions that stay open instead of pulling once per screen — why the request queue could not simply hold one, and how the group filter follows the room list |
| dead-code.md | code in the sync and relay stack that nothing calls, why each piece is still there, and which of it is a bug rather than a leftover |
| jvm-target.md | what desktop support cost, phased — why the native chain was already done, why an empty source set in our phoenix fork was the real blocker, and why DAO tests need none of it |
| material-design-conformance.md | what the M3 foundations actually require, measured against all 43 screens — the colour pairing that renders the app's own proposals invisible, and eight phases that put the decisions back in the theme |
Start with the ceremony if you are new to this area; the Marmot notes all assume it. Read the skipped-keys note before debugging any "the other device never got it" report — it is silent, and it looks like every other kind of delivery failure. The sync note stands alone, and the dead-code inventory reads as a follow-up to it. The batch-signing note is a phased plan that has been built: read it after the derivation note, whose one rule is the same one it is built around. The member chronicle note is a phased plan that has not been built, and reads as the membership note's unanswered half: what a member who joins late can be given, and the one thing they cannot. The jvm-target note is unrelated to all of them: it is a build and packaging story. The subgroups note is a phased plan that has been built; it assumes both shared-key notes and reads as the ceremony's second half — what a group does once it has a key, and what it can say about a group that does not yet. The multiple-parents note is research rather than a plan, and reads only after it: it takes the one assumption that note never had to state — that a lineage is settled at founding — and finds that dropping it costs less than founding a subgroup does, because no ceremony is involved. The Material Design note is a phased plan that has not been built, and is the only one about what the app looks like rather than what it does; read the jvm-target note first if you want to know why its adaptive-layout phase exists.